├── README.markdown ├── content ├── content.templates.php └── templates │ ├── class.tpl │ ├── viewEdit.xsl │ ├── viewIndex.xsl │ ├── xsl-html.tpl │ └── xsl-plain.tpl ├── extension.driver.php ├── extension.meta.xml ├── lang └── lang.de.php ├── lib ├── class.emailtemplate.php ├── class.emailtemplatemanager.php └── class.extensionpage.php └── license.txt /README.markdown: -------------------------------------------------------------------------------- 1 | # Email Template Manager 2 | 3 | * Author: Huib Keemink (@creativedutchmen) 4 | * Current maintainer: Michael Eichelsdoerfer (@michael-e) 5 | 6 | **A note on compatibility**: Releases > 7.3.3 officially require Symphony 2.7.x only because they implement the "Ignore attachment errors" option (see below). If you don't care about this checkbox having no effect, you can use the latest and greatest ETM with Symphony 2.6.x. (It might even work down to Symphony 2.4.0.) 7 | 8 | 9 | ## Contents 10 | 11 | * 1\. What's this? 12 | * 2\. Understanding the basics 13 | * 2.1 Templates and layouts 14 | * 2.2 Subject, recipients and reply-to 15 | * 2.3 Parameters 16 | * 3\. Tutorial: Contact Form 17 | 18 | 19 | ## 1. What's this? 20 | 21 | Using this extension it is possible to let Symphony send pretty emails using XSLT. 22 | 23 | 24 | ## 2. Understanding the basics 25 | 26 | Before using this extension, you should be familiar with the Symphony CMS. If you are not comfortable with datasources, events and parameters, please read the Symphony docs first. 27 | 28 | ### 2.1 Templates & layouts 29 | 30 | Templates created by this extension are similar to traditional Symphony pages. Just like with normal pages, you can attach datasources, whose XML you can process using XSLT. 31 | 32 | However, there are a few differences, too. Most emails will consist of two layouts: HTML and Plain. Every template created by the ETM has the option to select one or two layouts. For each layout, you will be able to set an XSLT template. 33 | 34 | **Warning: although it is possible, sending HTML-only templates is not recommended!** 35 | 36 | ### 2.2 Subject, recipients, from, reply-to and attachments 37 | 38 | The subject, from and reply-to settings in the config panel can contain XPath and parameters. If your XPath returns more than one piece of data, only the first result is used - you can not have more than one subject, for example. 39 | 40 | For the recipients field, on the other hand, you can select more than one recipient with a single piece of XPath. You can use the `Name `, `username`, `` and `"Name" ` syntaxes. Also, you can mix sources by combining queries with a comma: `username, email@domain.com, {/data/recipients/entry/email}` will create a valid list. 41 | 42 | You may override Symphony's "From Name" and "From Email Address" preferences. Changing the name can often be useful, but **faking the email address is dangerous and will not work (or be restricted to certain addresses) with most SMTP accounts**. Even if this works, you must be aware of possible implications in spam filters etc. Please consider this a "Pro" feature. 43 | 44 | For attachments, you may specify a comma-separated list of local file paths starting from the DOCROOT, e.g. `/workspace/media/foo.pdf`. It is also possible to include dynamic parts, e.g. using a (filtered) datasource: `/workspace/media/order-confirmations/order-{/data/order-confirmations/entry/@id}.pdf`. Like with the subject or reply-to settings, only the first matching node of an XPath expression is used. 45 | 46 | **Warning: it is not possible to mix the parameters and xpath syntax in one query: `{/data/$param}` will not work.** 47 | 48 | Tick the checkbox "Ignore attachment errors" if you like an email to be sent even if an attachment can not be loaded. This is useful if your email source contains an optional file upload, for example. 49 | 50 | Starting with version 7.5.0 ETM also allows HTTP(S) URLs for attachments, with a slight restriction: URLs must not contain commas, because the comma is supposed to be the separator for multiple attachments. It is possible, however, to mix local paths and URLs in the attachments string. 51 | 52 | Loading attachments via HTTP(S) can be rather inefficient. For mass emails (using the Email Newsletter Manager) it is strongly recommended to download all static files once and define them using the local path. 53 | 54 | ### 2.3 Parameters (event filters only!) 55 | 56 | If you are using filters, the ETM will automatically add a few parameters that you can use to filter your datasources: 57 | 58 | * `$etm-entry-id` will contain the id of the entry inserted by the event. 59 | You can use this parameter to filter a datasource and to email the data entered by the user. 60 | We will see this in action in the Contact Form in the Tutorials section of this manual. 61 | * `$etm-recipient` will contain the email address of the recipient of the email. 62 | If you are sending to more than one person, the ETM will loop over your recipients, and set this value for every email. 63 | Again, you will be able to filter your datasources with this parameter to include more information about the recipient. 64 | 65 | 66 | ## 3. Tutorial: Contact Form 67 | 68 | Using symphony and the "Send Notification Email" filter, it was already possible to send a quick preview of a response in a contact form. With ETM you are able to take this concept a few steps further. 69 | 70 | In this tutorial we will: 71 | 72 | * Send an email to an author in the symphony installation with a summary of the data in the contact form. 73 | * Email the visitor that his request for information has been received. 74 | 75 | ### 3.1 Setting up the section 76 | 77 | To store all requests, we will create a section called 'Responses'. Add three fields to this section: **Name** (text-input, required), **Email** (text-input, email, required) and **Body** (textarea). 78 | 79 | ### 3.2 Setting up the datasources 80 | 81 | Because we want our email to the author to contain pieces of the response, we will have to create a new datasource called **'Responses'** that gets its information from the 'Responses' section. At this moment, this datasource will return all responses ever created. This is not what we want - we want to only load the response we are emailing the author about. 82 | 83 | To do this, the ETM has a parameter you can use to filter your datasource: `$etm-entry-id`, this will contain the entry id of the entry created by the event. You can filter your datasource using this parameter (remember to filter by System ID). 84 | 85 | In the email, we want to include the body, the email address and the name of the response, so include those in your `Included Elements` selectbox. 86 | 87 | ### 3.3 Setting up the first template settings (notification) 88 | 89 | Now that we have created our section and our datasource, we can use this data to create a nice-looking email. To do this, create a new Email Template (Blueprints->Email Templates->Create New). 90 | 91 | This first email will be sent to the author, and will notify the author of a new response created. A nice name for this template can be **'Response-Notification'**. 92 | 93 | Next, select your 'Responses' datasource we created before in the `Datasources` selectbox. 94 | 95 | To keep things simple, select `Plain only` in the `Layouts` dropdown menu. If you want, you can also create a HTML template here, all the concepts are the same. 96 | 97 | Now for the interesting part. In the normal "Send Notification Email" filter, the subject would be predefined and static. With the ETM, you can set your own subject, and it can be dynamic, too. To see what happens, use `A new response has been posted by {/data/responses/entry/name}` as your subject. In this example, we have used the recipients name in the subject, creating a subject like: `A new response has been posted by Huib Keemink`, cool eh? 98 | 99 | Next, in the `Recipients` box, you can type the username of an author in Symphony. In my installation this is `huib`, but it can be anything that you have set. For more information about how to use the `Recipients` box, please look at section `2.2` of this manual. 100 | 101 | We have now setup all required settings for the Template, but there are two options left: `Reply-To Name` and `Reply-To Email Address`. To make replying extra easy, we can set these to contain the visitor's name and email. To do this, simply use `{/data/responses/entry/name}` in the `Reply-To Name` field, and `{/data/responses/entry/email}` in the `Reply-To Email Address` field. 102 | 103 | ### 3.4 Setting up the first template layout (notification) 104 | 105 | Now that we have configured the template to have a proper name, be sent to the right email address, have a descriptive subject and make replying extra easy, it's time to create our layout. 106 | 107 | Because we are only sending a plain email, you will have to configure only the Plain layout. In the `Body` textarea, you can insert your XSLT that will eventually be sent to the email address you provided. Below is an example of what you could use: 108 | 109 | 110 | 112 | 113 | 117 | 118 | 119 | Woohoo! Somebody wants information from us! 120 | 121 | To be more precise, asked this: 122 | 123 | 124 | 125 | --------------------------- 126 | To respond, you can send an email to: or reply to this email. 127 | 128 | 129 | 130 | 131 | 132 | ### 3.5 Setting up the second template settings (thank you message) 133 | 134 | First, create a new email template, and name it `Response Thankyou`. For this template, we can do pretty much all you want, the only thing that is really important is the `recipients` setting. Because we want this template to be sent to the sender of the form, we can use some XPath to select the email from the event. 135 | 136 | The ETM does not directly include the POST data in the event XML, so `{/data/events/_eventname_/post-data}` will not work. 137 | 138 | However, since we have already filtered the Responses datasource to only display this piece of information, we can use that. So, in the recipients pane, type: `{/data/responses/entry/name} <{/data/responses/entry/email}>`. 139 | 140 | ### 3.6 Setting up the second template layout (thank you message) 141 | 142 | We have now setup this template, all we need to do is edit the layout of this email. (The corresponding XSLT files are in `/workspace/email-templates`.) 143 | 144 | If you have selected to use only a Plain layout, as you did with the notification template, you can use something like this for the layout XSLT: 145 | 146 | 147 | 149 | 150 | 154 | 155 | 156 | Dear , 157 | 158 | Thank you for your interest in . 159 | We have received your inquiry, and will respond as quick as we can - usually within 24 hours. 160 | 161 | Regards, 162 | 163 | The ETeaM 164 | 165 | 166 | 167 | 168 | ### 3.7 Setting up the event 169 | 170 | Ok, so we have setup our section, our datasource and our template, let's make it work! 171 | 172 | In the event editor, select your templates in the list of event filters (they will be named `Send Email Template: Response-Notification` and `Send Email Template: Response Thankyou`). 173 | 174 | Now we are nearly done setting everything up, all we need to do is attach the event to a page and include the form (as usual). 175 | 176 | If everything went OK, submitting the form with a valid email should send out two emails: one to an author on the website, and one to the sender of the form. If it doesn't, please report your bugs at the [bugtracker](https://github.com/michael-e/email_template_manager/issues) 177 | 178 | ### 3.8 Conclusion 179 | 180 | In this (short) tutorial, we have looked at some of the basics of the ETM: creating templates, editing the layouts, setting dynamic recipients, subjects and reply-to headers. 181 | 182 | This tutorial has been written with the questions asked on the forum in mind. If you feel some parts have not been explained well, or should be added, feel free to [post your remarks](http://symphony-cms.com/discuss/thread/64323/). 183 | -------------------------------------------------------------------------------- /content/content.templates.php: -------------------------------------------------------------------------------- 1 | _XML = new XMLElement('data'); 24 | parent::__construct(Symphony::Engine()); 25 | $this->viewDir = ETVIEWS; 26 | } 27 | 28 | public function __actionNew() 29 | { 30 | $fields = $_POST['fields']; 31 | 32 | if (!$this->_validateConfig($fields, false, true)) { 33 | $this->_XML->appendChild($this->_validateConfig($fields, true, true)); 34 | $this->pageAlert( 35 | __('Could not save. Please correct errors below.'), 36 | Alert::ERROR 37 | ); 38 | } else { 39 | if ($fields['layouts'] === 'both') { 40 | $fields['layouts'] = array( 41 | 'html' => 'template.html.xsl', 42 | 'plain' => 'template.plain.xsl', 43 | ); 44 | } 45 | if ($fields['layouts'] === 'html') { 46 | $fields['layouts'] = array('html' => 'template.html.xsl'); 47 | } 48 | if ($fields['layouts'] === 'plain') { 49 | $fields['layouts'] = array('plain' => 'template.plain.xsl'); 50 | } 51 | if (EmailTemplateManager::create($fields)) { 52 | redirect(SYMPHONY_URL . '/extension/email_template_manager/templates/edit/' . EmailTemplateManager::getHandleFromName($fields['name']) . '/saved/'); 53 | } else { 54 | $this->pageAlert( 55 | __('Could not save: ' . EmailTemplateManager::$errorMsg), 56 | Alert::ERROR 57 | ); 58 | } 59 | } 60 | } 61 | 62 | public function __actionEdit() 63 | { 64 | $fields = $_POST['fields']; 65 | 66 | if (isset($_POST['action']['delete'])) { 67 | if (EmailTemplateManager::delete($this->_context[1])) { 68 | redirect(SYMPHONY_URL . '/extension/email_template_manager/templates/'); 69 | } else { 70 | $this->pageAlert( 71 | __('Could not delete: ' . EmailTemplateManager::$errorMsg), 72 | Alert::ERROR 73 | ); 74 | 75 | return; 76 | } 77 | } else { 78 | 79 | // Config editing 80 | if (empty($this->_context[2]) || ($this->_context[2] === 'saved')) { 81 | 82 | if (!$this->_validateConfig($fields)) { 83 | $this->_XML->appendChild($this->_validateConfig($fields, true, true)); 84 | $this->pageAlert( 85 | __('Could not save. Please correct errors below.'), 86 | Alert::ERROR 87 | ); 88 | } 89 | 90 | if ($fields['layouts'] === 'both') { 91 | $fields['layouts'] = array( 92 | 'html' => 'template.html.xsl', 93 | 'plain' => 'template.plain.xsl', 94 | ); 95 | } 96 | if ($fields['layouts'] === 'html') { 97 | $fields['layouts'] = array('html' => 'template.html.xsl'); 98 | } 99 | if ($fields['layouts'] === 'plain') { 100 | $fields['layouts'] = array('plain' => 'template.plain.xsl'); 101 | } 102 | 103 | if (EmailTemplateManager::editConfig($this->_context[1], $fields)) { 104 | redirect(SYMPHONY_URL . '/extension/email_template_manager/templates/edit/' . EmailTemplateManager::getHandleFromName($fields['name']) . '/saved/'); 105 | } else { 106 | $this->pageAlert( 107 | __('Could not save: ') . __(EmailTemplateManager::$errorMsg), 108 | Alert::ERROR 109 | ); 110 | } 111 | } 112 | } 113 | } 114 | 115 | public function __actionIndex() 116 | { 117 | if ($_POST['with-selected'] === 'delete') { 118 | foreach ((array) $_POST['items'] as $item => $status) { 119 | if (!EmailTemplateManager::delete($item)) { 120 | $this->pageAlert( 121 | __('Could not delete: ') . __(EmailTemplateManager::$errorMsg), 122 | Alert::ERROR 123 | ); 124 | } 125 | } 126 | } 127 | } 128 | 129 | public function __viewIndex() 130 | { 131 | $this->setPageType('index'); 132 | $this->setTitle(__('Symphony - Email Templates')); 133 | 134 | $this->appendSubheading(__('Email Templates'), Widget::Anchor( 135 | __('Create New'), SYMPHONY_URL . '/extension/email_template_manager/templates/new/', 136 | __('Create a new email template'), 'create button' 137 | )); 138 | 139 | // Fix for 2.4 and XSRF 140 | if ((Symphony::Configuration()->get('enable_xsrf', 'symphony') === 'yes') && 141 | (class_exists('XSRF'))) { 142 | $xsrf_input = new XMLElement('xsrf_input'); 143 | $xsrf_input->appendChild(XSRF::formToken()); 144 | $this->_XML->appendChild( 145 | $xsrf_input 146 | ); 147 | } 148 | 149 | $templates = new XMLElement('templates'); 150 | foreach (EmailTemplateManager::listAll() as $template) { 151 | $entry = new XMLElement('entry'); 152 | General::array_to_xml($entry, $template->about); 153 | General::array_to_xml($entry, $template->getProperties()); 154 | $entry->appendChild(new XMLElement('handle', $template->getHandle())); 155 | $templates->appendChild($entry); 156 | } 157 | $this->_XML->appendChild($templates); 158 | } 159 | 160 | public function __viewEdit($new = false) 161 | { 162 | $this->setPageType('form'); 163 | $this->setTitle(sprintf(__('Symphony - Email Templates - %s', array(), false), ucfirst($this->_context[1]))); 164 | 165 | if ((isset($this->_context[2]) && $this->_context[2] === 'saved') 166 | || (isset($this->_context[3]) && $this->_context[3] === 'saved')) { 167 | $this->pageAlert( 168 | __( 169 | __('Template updated at %1$s.'), 170 | array( 171 | Widget::Time()->generate(), 172 | ) 173 | ), 174 | Alert::SUCCESS 175 | ); 176 | } 177 | 178 | // Fix for 2.4 and XSRF 179 | if ((Symphony::Configuration()->get('enable_xsrf', 'symphony') === 'yes') && 180 | (class_exists('XSRF'))) { 181 | $xsrf_input = new XMLElement('xsrf_input'); 182 | $xsrf_input->appendChild(XSRF::formToken()); 183 | $this->_XML->appendChild( 184 | $xsrf_input 185 | ); 186 | } 187 | 188 | // Default page context 189 | $title = __('New Template'); 190 | $buttons = array(); 191 | $breadcrumbs = array( 192 | Widget::Anchor(__('Email Templates'), SYMPHONY_URL . '/extension/email_template_manager/templates/') 193 | ); 194 | 195 | // Edit config 196 | if (empty($this->_context[2]) || ($this->_context[2] === 'saved')) { 197 | $templates = new XMLElement('templates'); 198 | $template = EmailTemplateManager::load($this->_context[1]); 199 | if ($template) { 200 | $properties = $template->getProperties(); 201 | $title = $template->about['name']; 202 | $entry = new XMLElement('entry'); 203 | General::array_to_xml($entry, $template->about); 204 | General::array_to_xml($entry, $properties); 205 | $entry->appendChild(new XMLElement('handle', $template->getHandle())); 206 | $templates->appendChild($entry); 207 | 208 | // Create preview buttons 209 | $properties = $template->getProperties(); 210 | foreach ($properties['layouts'] as $layout => $file) { 211 | $buttons[] = Widget::Anchor( 212 | __('Preview %s layout', array($layout)), SYMPHONY_URL . '/extension/email_template_manager/templates/preview/' . $template->getHandle() . '/' . $layout . '/', 213 | __('Preview %s layout', array($layout)), 'button', null, array('target' => '_blank') 214 | ); 215 | } 216 | } elseif (!$new) { 217 | Administration::instance()->errorPageNotFound(); 218 | } 219 | $this->_XML->appendChild($templates); 220 | 221 | $datasources = new XMLElement('datasources'); 222 | $dsmanager = new DatasourceManager($this); 223 | foreach ($dsmanager->listAll() as $datasource) { 224 | $entry = new XMLElement('entry'); 225 | General::array_to_xml($entry, $datasource); 226 | $datasources->appendChild($entry); 227 | } 228 | $this->_XML->appendChild($datasources); 229 | General::array_to_xml($this->_XML, array('email-settings' => Symphony::Configuration()->get('email_' . EmailGatewayManager::getDefaultGateway()))); 230 | } else { 231 | Administration::instance()->errorPageNotFound(); 232 | } 233 | 234 | // Add page context 235 | $this->appendSubheading($title, $buttons); 236 | $this->insertBreadcrumbs($breadcrumbs); 237 | } 238 | 239 | public function __viewNew() 240 | { 241 | $this->_context[1] = 'New'; 242 | $this->_useTemplate = 'viewEdit'; 243 | $this->__viewEdit(true); 244 | } 245 | 246 | public function __viewPreview() 247 | { 248 | $this->_useTemplate = false; 249 | list(,$handle, $template) = $this->_context; 250 | $templates = EmailTemplateManager::load($handle); 251 | $output = $templates->preview($template); 252 | if ($template === 'plain' && !isset($_REQUEST['debug']) && !isset($_REQUEST['profile'])) { 253 | header('Content-Type:text/plain; charset=utf-8'); 254 | } 255 | echo $output; 256 | exit; 257 | } 258 | 259 | public function view() 260 | { 261 | $context = new XMLElement('context'); 262 | General::array_to_xml($context, $this->_context); 263 | $this->_XML->appendChild($context); 264 | parent::view(); 265 | } 266 | 267 | public function action() 268 | { 269 | if (isset($this->_context[2]) && $this->_context[2] === 'saved') { 270 | $this->_context[2] = null; 271 | } 272 | $fields = new XMLElement('fields'); 273 | General::array_to_xml($fields, (array) $_POST['fields']); 274 | $this->_XML->appendChild($fields); 275 | parent::action(); 276 | } 277 | 278 | public function build(array $context = array()) 279 | { 280 | parent::build($context); 281 | } 282 | 283 | protected function _validateConfig($config, $as_xml = false, $unique_name = false) 284 | { 285 | $errors = new XMLElement('errors'); 286 | if (!empty($config['name'])) { 287 | if ($unique_name && EmailTemplateManager::find(EmailTemplateManager::getHandleFromName($config['name']))) { 288 | $errors->appendChild(new XMLElement('name', __('A template with this name already exists.'))); 289 | if (!$as_xml) return false; 290 | } 291 | } else { 292 | $errors->appendChild(new XMLElement('name', __('This field can not be empty'))); 293 | if (!$as_xml) return false; 294 | } 295 | if (empty($config['subject'])) { 296 | $errors->appendChild(new XMLElement('subject', __('This field can not be empty'))); 297 | if (!$as_xml) return false; 298 | } 299 | 300 | return $errors; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /content/templates/class.tpl: -------------------------------------------------------------------------------- 1 | extends EmailTemplate 4 | { 5 | public $datasources = array( 6 | ); 7 | public $layouts = array( 8 | ); 9 | public $subject = ''; 10 | public $from_name = ''; 11 | public $from_email_address = ''; 12 | public $reply_to_name = ''; 13 | public $reply_to_email_address = ''; 14 | public $recipients = ''; 15 | public $attachments = ''; 16 | public $ignore_attachment_errors = ; 17 | 18 | public $editable = true; 19 | 20 | public $about = array( 21 | 'name' => '', 22 | 'version' => '', 23 | 'author' => array( 24 | 'name' => '', 25 | 'website' => '', 26 | 'email' => '' 27 | ), 28 | 'release-date' => '' 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /content/templates/viewEdit.xsl: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 |
12 |
13 | Template Settings 14 |
15 | 16 | 17 | invalid 18 | 19 | 20 | 35 | 36 |

37 |
38 |
39 | 65 |

Layouts will be able to use these datasources to build their content.

66 | 89 |

Only the layouts selected will be emailed.

90 |
91 |
92 | Email Settings 93 |

These settings are global settings for this template. They can be overwritten by extensions or custom events.

Use the {$param} and {/xpath/query} notation to include dynamic parts. It is not possible to combine the two syntaxes: {/xpath/$param} is not possible.

94 |
95 | 96 | 97 | invalid 98 | 99 | 100 | 115 | 116 |

117 |
118 | 119 |

Use the {$param} and {/xpath/query} notation to include dynamic parts. It is not possible to combine the two syntaxes. If the XPath returns more than one result, only the first is used

120 |
121 |
122 |
123 | 124 | 125 | invalid 126 | 127 | 128 | 144 | 145 |

146 |
147 | 148 |

Select multiple recipients by separating them with commas. This is also possible dynamically: {/data/authors/author/name} <{/data/authors/author/email}> will return: name <email@domain.com>, name2 <email2@domain.com>

149 |
150 |
151 |
152 |
153 | 154 | 155 | invalid 156 | 157 | 158 | 174 | 175 |

176 |
177 |
178 |
179 | 180 | 181 | invalid 182 | 183 | 184 | 200 | 201 |

202 |
203 | 204 |

Faking this address is dangerous. It may cause issues with spam filters and even break sending, especially via SMTP.

205 |
206 |
207 |
208 |
209 |
210 | 211 | 212 | invalid 213 | 214 | 215 | 231 | 232 |

233 |
234 |
235 |
236 | 237 | 238 | invalid 239 | 240 | 241 | 257 | 258 |

259 |
260 |
261 |
262 |
263 | 264 | 265 | invalid 266 | 267 | 268 | 284 | 285 |

286 |
287 | 288 |

Select multiple attachments by separating them with commas. For each file define either a URL or a local path starting from the DOCROOT, e.g. /workspace/media/foo.pdf. It is also possible to include dynamic parts. URLs and local paths must not contain commas.

289 |
290 |
291 |
292 | 293 | 294 | invalid 295 | 296 | 297 | 311 | 312 |

313 |
314 | 315 |

With the above option an email will be sent even if an attachment can not be loaded.

316 |
317 |
318 |
319 |
320 | 321 | 322 | 323 | 324 | 325 | 326 | Save Changes 327 | Create Template 328 | 329 | 330 | 331 | 332 | 333 | 334 |
335 |
336 |
337 | 338 |
339 | -------------------------------------------------------------------------------- /content/templates/viewIndex.xsl: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 |
NamePreview
26 | None found 27 |
32 |
33 | 34 | 35 | 36 |
37 |
38 | 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 | -------------------------------------------------------------------------------- /content/templates/xsl-html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /content/templates/xsl-plain.tpl: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /extension.driver.php: -------------------------------------------------------------------------------- 1 | __('Blueprints'), 13 | 'name' => __('Email Templates'), 14 | 'link' => '/templates/' 15 | ) 16 | ); 17 | } 18 | 19 | public function getSubscribedDelegates() 20 | { 21 | return array( 22 | array( 23 | 'page' => '/blueprints/events/edit/', 24 | 'delegate' => 'AppendEventFilter', 25 | 'callback' => 'appendEventFilter' 26 | ), 27 | array( 28 | 'page' => '/blueprints/events/new/', 29 | 'delegate' => 'AppendEventFilter', 30 | 'callback' => 'appendEventFilter' 31 | ), 32 | array( 33 | 'page' => '/frontend/', 34 | 'delegate' => 'EventFinalSaveFilter', 35 | 'callback' => 'eventFinalSaveFilter' 36 | ), 37 | array( 38 | 'page' => '/blueprints/events/edit/', 39 | 'delegate' => 'AppendEventFilterDocumentation', 40 | 'callback' => 'appendEventFilterDocumentation' 41 | ), 42 | array( 43 | 'page' => '/blueprints/datasources/', 44 | 'delegate' => 'DatasourcePostEdit', 45 | 'callback' => 'datasourcePostEdit' 46 | ), 47 | ); 48 | } 49 | 50 | public function install() 51 | { 52 | if (!is_dir(WORKSPACE . '/email-templates')) { 53 | try { 54 | mkdir(WORKSPACE . '/email-templates'); 55 | } catch (Exception $e) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | 63 | public function uninstall() 64 | { 65 | try { 66 | General::deleteDirectory(WORKSPACE.'/email-templates'); 67 | } catch (Exception $e) { 68 | return false; 69 | } 70 | 71 | return true; 72 | } 73 | 74 | public function datasourcePostEdit($file) 75 | { 76 | $ds_handle = DatasourceManager::__getHandleFromFileName(basename($file['file'])); 77 | $templates = EmailTemplateManager::listAll(); 78 | foreach ($templates as $template) { 79 | $config = $template->getProperties(); 80 | if (!is_null($file['previous_file']) && ($key = array_search(DatasourceManager::__getHandleFromFilename(basename($file['previous_file'])), $config['datasources'])) !== false) { 81 | $config['datasources'][$key] = $ds_handle; 82 | 83 | return EmailTemplateManager::editConfig($template->getHandle(), array_merge($template->getAbout(), $config)); 84 | } 85 | } 86 | } 87 | 88 | public function appendEventFilter($context) 89 | { 90 | $templates = EmailTemplateManager::listAll(); 91 | if (empty($templates)) return; 92 | foreach ($templates as $template) { 93 | $tmp[$template->getHandle()] = $template; 94 | } 95 | $templates = is_array($tmp)?$tmp:array(); 96 | ksort($templates, SORT_STRING); 97 | foreach ($templates as $template) { 98 | $handle = 'etm-' . $template->getHandle(); 99 | $selected = (in_array($handle, $context['selected'])); 100 | $context['options'][] = array( 101 | $handle, $selected, General::sanitize('Send Email Template: ' . $template->getName()) 102 | ); 103 | } 104 | } 105 | 106 | public function eventFinalSaveFilter($context) 107 | { 108 | $templates = EmailTemplateManager::listAll(); 109 | foreach ($templates as $template) { 110 | $handle = 'etm-' . $template->getHandle(); 111 | if (in_array($handle, (array) $context['event']->eParamFILTERS)) { 112 | if (($response = $this->_sendEmail($template, $context)) !== false) { 113 | $context['errors'][] = array('etm-' . $template->getHandle(), ($response['sent']>0), null, $response); 114 | } 115 | } 116 | } 117 | } 118 | 119 | protected function _sendEmail($template, $context) 120 | { 121 | try { 122 | $template->addParams(array('etm-entry-id' => $context['entry']->get('id'))); 123 | Symphony::Engine()->Page()->_param['etm-entry-id'] = $context['entry']->get('id'); 124 | 125 | //Add POST as page parameters 126 | foreach ($context['fields'] as $field => $val) { 127 | if (is_array($val)) { 128 | foreach ($val as $key => $value) { 129 | if (is_array($value)) $value = implode($value,','); 130 | $template->addParams(array('etm-post-'.$field.'.'.$key => $value)); 131 | Symphony::Engine()->Page()->_param['etm-post-'.$field.'.'.$key] = $value; 132 | } 133 | } else { 134 | $template->addParams(array('etm-post-'.$field => $val)); 135 | Symphony::Engine()->Page()->_param['etm-post-'.$field] = $val; 136 | } 137 | } 138 | 139 | $xml = $template->processDatasources(); 140 | 141 | $about = $context['event']->about(); 142 | General::array_to_xml($xml, array('events' => array($about['name'] => array('post-values' => $context['fields'])))); 143 | 144 | $template->setXML($xml->generate()); 145 | 146 | $template->parseProperties(); 147 | $properties = $template->getParsedProperties(); 148 | $recipients = array_unique((array) $properties['recipients']); 149 | 150 | $sent = 0; 151 | if (count($recipients) > 0) { 152 | foreach ($recipients as $name => $emailaddr) { 153 | try { 154 | $email = Email::create(); 155 | $template->addParams(array('etm-recipient' => $emailaddr)); 156 | $xml = $template->processDatasources(); 157 | 158 | $about = $context['event']->about(); 159 | General::array_to_xml($xml, array('events' => array($about['name'] => array('post-values' => $context['fields'])))); 160 | 161 | $template->setXML($xml->generate()); 162 | $template->recipients = $emailaddr; 163 | 164 | $content = $template->render(); 165 | 166 | if (!empty($content['subject'])) { 167 | $email->subject = $content['subject']; 168 | } else { 169 | throw new EmailTemplateException(__('Can not send emails without a subject')); 170 | } 171 | 172 | if (!empty($content['from-name'])) { 173 | $email->sender_name = $content['from-name']; 174 | } 175 | 176 | if (!empty($content['from-email-address'])) { 177 | $email->sender_email_address = $content['from-email-address']; 178 | } 179 | 180 | if (isset($content['reply-to-name'])) { 181 | $email->reply_to_name = $content['reply-to-name']; 182 | } 183 | 184 | if (isset($content['reply-to-email-address'])) { 185 | $email->reply_to_email_address = $content['reply-to-email-address']; 186 | } 187 | 188 | if (isset($content['plain'])) { 189 | $email->text_plain = $content['plain']; 190 | } 191 | if (isset($content['html'])) { 192 | $email->text_html = $content['html']; 193 | } 194 | if (!empty($content['attachments'])) { 195 | $email->attachments = $content['attachments']; 196 | } 197 | if (isset($content['ignore-attachment-errors'])) { 198 | $email->validate_attachment_errors = !$content['ignore-attachment-errors']; 199 | } 200 | 201 | require_once(TOOLKIT . '/util.validators.php'); 202 | if (General::validateString($emailaddr, $validators['email'])) { 203 | $email->recipients = array($name => $emailaddr); 204 | } else { 205 | throw new EmailTemplateException(__('Email address invalid:') . ' ' . $emailaddr); 206 | } 207 | 208 | $email->send(); 209 | $sent++; 210 | } catch (EmailTemplateException $e) { 211 | Symphony::Log()->pushToLog(__('Email Template Manager: ') . $e->getMessage(), null, true); 212 | //$context['errors'][] = array('etm-' . $template->getHandle() . '-' . Lang::createHandle($emailaddr), false, $e->getMessage()); 213 | continue; 214 | } 215 | } 216 | } else { 217 | throw new EmailTemplateException('Can not send an email to nobody, please set a recipient.'); 218 | } 219 | } catch (EmailTemplateException $e) { 220 | $context['errors'][] = array('etm-' . $template->getHandle(), false, $e->getMessage()); 221 | 222 | return false; 223 | } catch (EmailValidationException $e) { 224 | $context['errors'][] = array('etm-' . $template->getHandle(), false, $e->getMessage()); 225 | 226 | return false; 227 | } catch (EmailGatewayException $e) { 228 | $context['errors'][] = array('etm-' . $template->getHandle(), false, $e->getMessage()); 229 | 230 | return false; 231 | } 232 | 233 | return array('total' => count($recipients), 'sent' => $sent); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /extension.meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Email Template Manager 4 | Emails need pages no more 5 | https://github.com/creativedutchmen/email_template_manager 6 | http://symphony-cms.com/discuss/thread/64323/ 7 | 8 | Email 9 | 10 | 11 | 12 | Huib Keemink 13 | http://www.creativedutchmen.com 14 | 15 | 16 | 17 | Email Template Manager: Emails need pages no more 18 | 19 | 20 | 21 | 24 | 25 | 26 | 30 | 31 | 32 | 36 | 37 | 38 | 41 | 42 | 43 | 46 | 47 | 48 | 54 | 55 | 56 | 59 | 60 | 61 | 64 | 65 | 66 | 70 | 71 | 72 | 75 | 76 | 77 | 80 | 81 | 82 | 85 | 86 | 87 | 90 | 91 | 92 | 95 | 96 | 97 | 102 | 103 | 104 | 107 | 108 | 109 | 113 | 114 | 115 | 120 | 121 | 122 | 125 | 126 | 127 | 131 | 132 | 133 | 136 | 137 | 138 | 141 | 142 | 143 | 146 | 147 | 148 | 152 | 153 | 154 | 157 | 158 | 159 | 162 | 163 | 164 | 167 | 168 | 169 | 170 | 173 | 174 | 175 | 181 | 182 | 183 | 188 | 189 | 190 | 193 | 194 | 195 | 199 | 200 | 201 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /lang/lang.de.php: -------------------------------------------------------------------------------- 1 | '', 5 | 'author' => array( 6 | 'name' => 'Michael Eichelsdörfer', 7 | 'email' => 'info@michael-eichelsdoerfer.de', 8 | 'website' => '' 9 | ), 10 | 'release-date' => '2011-04-24' 11 | ); 12 | 13 | /** 14 | * Email Template Manager 15 | */ 16 | $dictionary = array( 17 | 18 | 'Email Templates' => 19 | false, 20 | 21 | 'Email sent successfully.' => 22 | 'E-Mail erfolgreich verschickt.', 23 | 24 | 'Could not save. Please correct errors below.' => 25 | 'Speichern fehlgeschlagen; Bitte die unten stehenden Fehler korrigieren.', 26 | 27 | 'Could not save: ' => 28 | 'Speichern fehlgeschlagen: ', 29 | 30 | 'Could not delete: ' => 31 | 'Löschen fehlgeschlagen: ', 32 | 33 | 'Body is a required field' => 34 | 'Body ist ein Pflichtfeld', 35 | 36 | 'Symphony - Email Templates' => 37 | false, 38 | 39 | 'Symphony - Email Templates - %s' => 40 | false, 41 | 42 | 'Template updated at %1$s.' => 43 | 'Template um %1$s aktualisiert.', 44 | 45 | 'A template with this name already exists.' => 46 | 'Ein Template mit diesem Namen existiert bereits.', 47 | 48 | 'This field can not be empty' => 49 | 'Dieses Feld darf nicht leer sein', 50 | 51 | 'Email Template Manager' => 52 | false, 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /lib/class.emailtemplate.php: -------------------------------------------------------------------------------- 1 | addParams(array( 29 | 'today' => DateTimeObj::get('Y-m-d'), 30 | 'current-time' => DateTimeObj::get('H:i'), 31 | 'this-year' => DateTimeObj::get('Y'), 32 | 'this-month' => DateTimeObj::get('m'), 33 | 'this-day' => DateTimeObj::get('d'), 34 | 'timezone' => DateTimeObj::get('P'), 35 | 'website-name' => Symphony::Configuration()->get('sitename', 'general'), 36 | 'root' => URL, 37 | 'workspace' => URL . '/workspace' 38 | )); 39 | } 40 | 41 | public function addParams($params = array()) 42 | { 43 | if (!is_array($params)) return false; 44 | 45 | return ($this->setRuntimeParam(array_merge($this->_param, $params))); 46 | } 47 | 48 | public function getAbout() 49 | { 50 | return $this->about; 51 | } 52 | 53 | public function getName() 54 | { 55 | return $this->about['name']; 56 | } 57 | 58 | public function getHandle() 59 | { 60 | return Lang::createHandle($this->getName()); 61 | } 62 | 63 | public function processDatasources() 64 | { 65 | if (is_null($this->_frontendPage)) $this->_frontendPage = new FrontendPage(Symphony::Engine()); 66 | 67 | $this->_frontendPage->_param = $this->_param; 68 | 69 | $xml = new XMLElement('data'); 70 | $xml->setIncludeHeader(true); 71 | $this->_frontendPage->processDatasources(implode(', ',$this->datasources), $xml); 72 | $env = $this->_frontendPage->Env(); 73 | if (isset($env['pool'])) { 74 | foreach ((array) $env['pool'] as $name => $val) { 75 | $tmp[$name] = implode(', ', (array) $val); 76 | } 77 | $this->addParams($tmp); 78 | } 79 | 80 | return $xml; 81 | } 82 | 83 | public function evalXPath($xpath_string, $multiple = false) 84 | { 85 | $dom = new DOMDocument(); 86 | $dom->strictErrorChecking = false; 87 | $dom->loadXML($this->getXML()); 88 | $xpath = new DOMXPath($dom); 89 | if ($multiple === true) { 90 | 91 | foreach (array_keys($this->_param) as $param) { 92 | $search_strings[] = '{$' . $param . '}'; 93 | } 94 | 95 | $xpath_string = trim($xpath_string); 96 | $str = str_replace($search_strings, $this->_param, $xpath_string); 97 | $replacements = array(); 98 | preg_match_all('/\{[^\}\$]+\}/', $str, $matches); 99 | $str = array($str); 100 | if (is_array($matches[0]) && !empty($matches[0])) { 101 | foreach ($matches[0] as $match) { 102 | $results = @$xpath->evaluate(trim($match, '{}')); 103 | if (is_object($results)) { 104 | if ($results->length > 0) { 105 | if (count($str) === 1) { 106 | $str = array_fill(0, $results->length, $str[0]); 107 | } 108 | if (count($str) === $results->length) { 109 | foreach ($results as $offset => $result) { 110 | $str[$offset] = str_replace($match, trim($result->textContent), $str[$offset]); 111 | } 112 | } else { 113 | throw new EmailTemplateException('XPath matching failed. Number of returned values in queries do not match'); 114 | } 115 | } elseif ($results->length <= 0) { 116 | foreach ($str as $offset => $val) { 117 | $str[$offset] = ''; 118 | } 119 | Symphony::Log()->pushToLog(__('Email Template Manager') . ': ' . ' Xpath query '.$match.' did not return any results, skipping. ', 100, true); 120 | } 121 | } else { 122 | if (empty($results)) { 123 | $results = ''; 124 | } 125 | foreach ($str as $offset => $val) { 126 | $str[$offset] = str_replace($match, trim($results), $str[$offset]); 127 | } 128 | } 129 | } 130 | } 131 | 132 | //split the results at the end otherwise it might split an xpath concat function 133 | $ret = explode(',', implode(',', $str)); 134 | 135 | return $ret; 136 | } else { 137 | $search_strings = array(); 138 | foreach (array_keys($this->_param) as $param) { 139 | $search_strings[] = '{$' . $param . '}'; 140 | } 141 | $str = str_replace($search_strings, $this->_param, $xpath_string); 142 | $replacements = array(); 143 | preg_match_all('/\{[^\}\$]+\}/', $str, $matches); 144 | if (is_array($matches[0])) { 145 | $dom = new DOMDocument(); 146 | $dom->strictErrorChecking = false; 147 | $dom->loadXML($this->getXML()); 148 | $xpath = new DOMXPath($dom); 149 | foreach ($matches[0] as $match) { 150 | $results = @$xpath->evaluate('string(' . trim($match, '{}') . ')'); 151 | if (!is_null($results)) { 152 | $replacements[$match] = trim($results); 153 | } else { 154 | $replacements[$match] = ''; 155 | } 156 | } 157 | 158 | return str_replace(array_keys($replacements), array_values($replacements), $str); 159 | } 160 | } 161 | } 162 | 163 | public function render($layouts = array('plain', 'html'), $is_preview = false) 164 | { 165 | if (!is_array($layouts)) { 166 | $layouts = array($layouts); 167 | } 168 | if (isset($this->datasources) && isset($this->layouts)) { 169 | $result = array(); 170 | 171 | if (is_array($_GET) && !empty($_GET)) { 172 | foreach ($_GET as $key => $val) { 173 | if (!in_array($key, array('symphony-page', 'debug', 'profile'))) $this->_param['url-' . $key] = $val; 174 | } 175 | } 176 | 177 | if (is_null($this->getXML())) { 178 | try { 179 | $this->setXML($this->processDatasources()->generate(true, 0)); 180 | } catch (Exception $e) { 181 | $error = $this->getError(); 182 | throw new EmailTemplateException('Error including XML for rendering: ' . $e->getMessage()); 183 | } 184 | } 185 | 186 | if (!$is_preview) { 187 | $this->parseProperties(); 188 | } 189 | $properties = $this->getParsedProperties(); 190 | 191 | foreach ($this->layouts as $type => $layout) { 192 | if (in_array(strtolower($type), array_map('strtolower', $layouts))) { 193 | $xsl = ' 194 | 195 | 196 | '; 197 | $this->setXSL($xsl, false); 198 | $res = $this->generate(); 199 | if ($res) { 200 | $result[strtolower($type)] = $res; 201 | } else { 202 | $error = $this->getError(); 203 | throw new EmailTemplateException('Error compiling xml with xslt: ' . $error[1]['message']); 204 | } 205 | 206 | } 207 | } 208 | 209 | return array_merge($result, $properties); 210 | } 211 | } 212 | 213 | public function generate($page = null) 214 | { 215 | /** 216 | * Immediately before generating the output. Provided with the template object, XML and XSLT 217 | * @delegate EmailTemplateOutputPreGenerate 218 | * @param string $context 219 | * '/extension/email_template_manager/' 220 | * @param EmailTemplate $page 221 | * This EmailTemplate object, by reference 222 | * @param XMLElement $xml 223 | * This template's XML, including the Parameters, Datasource and Event XML, 224 | * by reference as an XMLElement 225 | * @param string $xsl 226 | * This template's XSLT, by reference 227 | */ 228 | Symphony::ExtensionManager()->notifyMembers( 229 | 'EmailTemplateOutputPreGenerate', 230 | '/extension/email_template_manager/', 231 | array( 232 | 'page' => &$this, 233 | 'xml' => &$this->_xml, 234 | 'xsl' => &$this->_xsl 235 | ) 236 | ); 237 | 238 | $output = parent::generate(); 239 | 240 | /** 241 | * Immediately after generating the output. Provided with string containing the output 242 | * @delegate EmailTemplateOutputPostGenerate 243 | * @param string $context 244 | * '/extension/email_template_manager/' 245 | * @param string $output 246 | * The generated output of this template, i.e. a string, passed by reference 247 | */ 248 | Symphony::ExtensionManager()->notifyMembers( 249 | 'EmailTemplateOutputPostGenerate', 250 | '/extension/email_template_manager/', 251 | array( 252 | 'output' => &$output 253 | ) 254 | ); 255 | 256 | return $output; 257 | } 258 | 259 | public function preview($template) 260 | { 261 | $output = $this->render($template, true); 262 | $output = $output[$template]; 263 | $devkit = null; 264 | Symphony::ExtensionManager()->notifyMembers( 265 | 'FrontendDevKitResolve', '/frontend/', 266 | array( 267 | 'full_generate' => true, 268 | 'devkit' => &$devkit 269 | ) 270 | ); 271 | if (!is_null($devkit)) { 272 | $devkit->prepare($this, array('title' => $this->getName(), 'filelocation' => dirname(EmailTemplateManager::find($this->getHandle())) . '/' . EmailTemplateManager::getFileNameFromLayout($template)), $this->_xml, $this->_param, $output); 273 | 274 | return $devkit->build(); 275 | } 276 | 277 | return $output; 278 | } 279 | 280 | public function __set($var, $val) 281 | { 282 | if (property_exists($this, $var)) { 283 | $prop = new ReflectionProperty($this, $var); 284 | if ($prop->isPublic()) { 285 | $this->$var = $val; 286 | unset($this->_parsedProperties[$var]); 287 | } 288 | } 289 | } 290 | 291 | public function parseProperties() 292 | { 293 | if (empty($this->_parsedProperties['recipients'])) { 294 | $recipients = $this->evalXPath($this->recipients, true); 295 | foreach ($recipients as $recipient) { 296 | if (strlen($recipient) > 0) { 297 | if (strpos($recipient, '@') !== false) { 298 | // NAME 299 | if ((($start = strpos($recipient, '<')) !== false) && (($stop = strpos($recipient, '>')) !== false)) { 300 | $name = trim(substr($recipient, 0, $start), '"< '); 301 | if (strlen($name) === 0) { 302 | $name = count((array) $rcpts); 303 | } 304 | $rcpts[trim($name)] = trim(substr($recipient, $start+1, $stop - ($start+1))); 305 | } 306 | // email@domain 307 | else{ 308 | $rcpts[] = trim($recipient); 309 | } 310 | } 311 | // username 312 | else{ 313 | $author = AuthorManager::fetchByUserName(trim($recipient)); 314 | if (is_a($author, 'Author')) { 315 | $rcpts[trim($author->get('first_name') . ' '. $author->get('last_name'))] = $author->get('email'); 316 | } else { 317 | Symphony::Log()->pushToLog(__('Email Template Manager') . ': ' . ' Recipient is recognised as a username, but the user can not be found: ' . $recipient , 100, true); 318 | } 319 | } 320 | } else { 321 | Symphony::Log()->pushToLog(__('Email Template Manager') . ': ' . ' Recipient is empty, skipping.' , 100, true); 322 | } 323 | } 324 | if (!empty($rcpts)) { 325 | $this->_parsedProperties['recipients'] = $rcpts; 326 | } else { 327 | Symphony::Log()->pushToLog(__('Email Template Manager') . ': ' . ' No valid recipients are selected, can not send emails.' , 100, true); 328 | } 329 | } 330 | 331 | if (empty($this->_parsedProperties['subject'])) { 332 | $this->_parsedProperties['subject'] = $this->evalXPath($this->subject, false); 333 | //$this->addParams(array('etm-subject' => $this->_parsedProperties['subject'])); 334 | } 335 | 336 | if (empty($this->_parsedProperties['from-name'])) { 337 | $this->_parsedProperties['from-name'] = $this->evalXPath($this->from_name, false); 338 | } 339 | 340 | if (empty($this->_parsedProperties['from-email-address'])) { 341 | $this->_parsedProperties['from-email-address'] = $this->evalXPath($this->from_email_address, false); 342 | } 343 | 344 | if (empty($this->_parsedProperties['reply-to-name'])) { 345 | $this->_parsedProperties['reply-to-name'] = $this->evalXPath($this->reply_to_name, false); 346 | //$this->addParams(array('etm-reply-to-name' => $this->_parsedProperties['reply-to-name'])); 347 | } 348 | 349 | if (empty($this->_parsedProperties['reply-to-email-address'])) { 350 | $this->_parsedProperties['reply-to-email-address'] = $this->evalXPath($this->reply_to_email_address, false); 351 | //$this->addParams(array('etm-reply-to-email-address' => $this->_parsedProperties['reply-to-email-address'])); 352 | } 353 | 354 | if (empty($this->_parsedProperties['attachments'])) { 355 | $atts_config_eval = $this->evalXPath($this->attachments, false); 356 | $attachments = array(); 357 | if (!empty($atts_config_eval)) { 358 | $atts = explode(',', $atts_config_eval); 359 | foreach ($atts as $att) { 360 | if (filter_var($att, FILTER_VALIDATE_URL)) { 361 | $attachments[] = $att; 362 | } else { 363 | $attachments[] = DOCROOT . $att; 364 | } 365 | } 366 | } 367 | $this->_parsedProperties['attachments'] = $attachments; 368 | } 369 | 370 | if (empty($this->_parsedProperties['ignore-attachment-errors'])) { 371 | $this->_parsedProperties['ignore-attachment-errors'] = $this->ignore_attachment_errors; 372 | } 373 | } 374 | 375 | public function getParsedProperties() 376 | { 377 | return $this->_parsedProperties; 378 | } 379 | 380 | public function getProperties() 381 | { 382 | return array( 383 | 'from-name' => $this->from_name, 384 | 'from-email-address' => $this->from_email_address, 385 | 'reply-to-name' => $this->reply_to_name, 386 | 'reply-to-email-address' => $this->reply_to_email_address, 387 | 'attachments' => $this->attachments, 388 | 'ignore-attachment-errors' => $this->ignore_attachment_errors, 389 | 'subject' => $this->subject, 390 | 'recipients' => $this->recipients, 391 | 'datasources' => $this->datasources, 392 | 'layouts' => $this->layouts 393 | ); 394 | } 395 | 396 | public function setXML($xml, $isFile = false) 397 | { 398 | $this->_parsedProperties = array(); 399 | 400 | return parent::setXML($xml); 401 | } 402 | } 403 | 404 | class EmailTemplateException extends Exception 405 | { 406 | 407 | } 408 | -------------------------------------------------------------------------------- /lib/class.emailtemplatemanager.php: -------------------------------------------------------------------------------- 1 | _writeConfig($handle, $etm->_parseConfigTemplate($handle, $config), true)) return false; 79 | if (!$etm->_writeLayout($handle, 'Plain', file_get_contents(ETMDIR . '/content/templates/xsl-plain.tpl'), true)) return false; 80 | if (!$etm->_writeLayout($handle, 'HTML', file_get_contents(ETMDIR . '/content/templates/xsl-html.tpl'), true)) return false; 81 | 82 | Symphony::ExtensionManager()->notifyMembers( 83 | 'EmailTemplatePostCreate', 84 | '/extension/email_template_manager/', 85 | array( 86 | 'config' => $config 87 | ) 88 | ); 89 | 90 | return true; 91 | } else { 92 | self::$errorMsg = 'Dir ' . EMAILTEMPLATES . "/$handle already exists."; 93 | 94 | return false; 95 | } 96 | } else { 97 | self::$errorMsg = "Template $handle already exists."; 98 | 99 | return false; 100 | } 101 | } 102 | 103 | public static function editConfig($handle, $config) 104 | { 105 | if ($template = self::load($handle)) { 106 | if ($template->editable === true) { 107 | $etm = new EmailTemplateManager(); 108 | if ($etm->_writeConfig($handle, $etm->_parseConfigTemplate($handle, $config))) { 109 | 110 | $old_dir = dirname(self::find($handle)); 111 | $new_dir = dirname($old_dir) . '/' . self::getHandleFromName($config['name']); 112 | 113 | if (self::getHandleFromName($config['name']) !== $handle) { 114 | if (!is_dir($new_dir)) { 115 | if (!rename($old_dir, $new_dir)) return false; 116 | 117 | Symphony::ExtensionManager()->notifyMembers( 118 | 'EmailTemplatePostSave', 119 | '/extension/email_template_manager/', 120 | array( 121 | 'old_handle' => $handle, 122 | 'config' => $config 123 | ) 124 | ); 125 | 126 | return rename($new_dir . '/' . self::getFileNameFromHandle($handle), $new_dir . '/' . self::getFileNameFromHandle(self::getHandleFromName($config['name']))); 127 | } 128 | } 129 | 130 | return true; 131 | } else { 132 | return false; 133 | } 134 | } else { 135 | self::$errorMsg = "Template $handle is set to read-only mode."; 136 | 137 | return false; 138 | } 139 | } else { 140 | self::$errorMsg = "Template $handle can not be found."; 141 | 142 | return false; 143 | } 144 | } 145 | 146 | // No longer used in 7.0 147 | public static function editLayout($handle, $layout, $content) 148 | { 149 | if ($template = self::load($handle)) { 150 | if (in_array($layout, array_keys($template->layouts), true)) { 151 | return self::_writeLayout($handle, $layout, $content); 152 | } else { 153 | self::$errorMsg = "Layout $layout is not set with template $handle."; 154 | 155 | return false; 156 | } 157 | } else { 158 | self::$errorMsg = "Template $handle not found."; 159 | 160 | return false; 161 | } 162 | } 163 | 164 | public static function delete($handle) 165 | { 166 | $dir = dirname(self::find($handle)); 167 | if (is_dir($dir) && is_writeable($dir)) { 168 | try { 169 | if (!(($files = @scandir($dir)) && count($files) <= 2)) { 170 | foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)) as $filename => $cur) { 171 | if (is_dir($filename)) { 172 | rmdir($filename); 173 | } elseif (is_file($filename)) { 174 | unlink($filename); 175 | } 176 | } 177 | } 178 | 179 | return rmdir($dir); 180 | } catch (Exception $e) { 181 | self::$errorMsg = "Directory $dir could not be removed. Please check permissions."; 182 | 183 | return false; 184 | } 185 | } else { 186 | self::$errorMsg = "Template $handle can not be found."; 187 | 188 | return false; 189 | } 190 | } 191 | 192 | public static function listAll() 193 | { 194 | $result = array(); 195 | 196 | foreach (new DirectoryIterator(EMAILTEMPLATES) as $dir) { 197 | if ($dir->isDir() && !$dir->isDot()) { 198 | if (file_exists($dir->getPathname() . '/' . self::getFileNameFromHandle($dir->getFilename()))) { 199 | $result[$dir->getFileName()] = self::load($dir->getFileName()); 200 | } 201 | } 202 | } 203 | 204 | foreach (ExtensionManager::listInstalledHandles() as $extension) { 205 | if (is_dir(EXTENSIONS . '/' . $extension . '/email-templates')) { 206 | foreach (new DirectoryIterator(EXTENSIONS . '/' . $extension . '/email-templates') as $dir) { 207 | if ($dir->isDir() && !$dir->isDot()) { 208 | if (file_exists($dir->getPathname() . '/' . self::getFileNameFromHandle($dir->getFilename()))) { 209 | $result[$dir->getFileName()] = self::load($dir->getFileName()); 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | ksort($result, SORT_STRING); 217 | 218 | return $result; 219 | } 220 | 221 | public static function getClassNameFromHandle($handle) 222 | { 223 | return sprintf('%sEmailTemplate', str_replace('-', '_', ucfirst(strtolower($handle)))); 224 | } 225 | 226 | public static function getHandleFromFilename($filename) 227 | { 228 | return sscanf($filename, 'class.%[^.php].php'); 229 | } 230 | 231 | public static function getFileNameFromHandle($handle) 232 | { 233 | return sprintf('class.%s.php', strtolower($handle)); 234 | } 235 | 236 | public static function getHandleFromName($name) 237 | { 238 | return Lang::createHandle($name); 239 | } 240 | public static function getFileNameFromLayout($layout = 'html') 241 | { 242 | return sprintf('template.%s.xsl', strtolower($layout)); 243 | } 244 | 245 | public static function about($name) 246 | { 247 | $classname = self::__getClassName($name); 248 | $path = self::__getDriverPath($name); 249 | 250 | if (!@file_exists($path)) return false; 251 | 252 | require_once($path); 253 | 254 | $handle = self::__getHandleFromFilename(basename($path)); 255 | 256 | if (is_callable(array($classname, 'about'))) { 257 | $about = call_user_func(array($classname, 'about')); 258 | 259 | return array_merge($about, array('handle' => $handle)); 260 | } 261 | 262 | } 263 | 264 | /** 265 | * Writes configuration values to the template configuration file. 266 | * 267 | * The name of the template to write configuration values to 268 | * @param string $handle 269 | * The configuration to write 270 | * @param string $contents 271 | * The location to write to, defaults to the workspace dir 272 | * @param string $file 273 | * @param bool $overwrite 274 | * 275 | * @return bool 276 | */ 277 | protected function _writeConfig($handle, $contents, $new = false) 278 | { 279 | if ($dir = ($new) ? EMAILTEMPLATES . '/' . $handle : dirname(self::find($handle))) { 280 | if (is_dir($dir) && is_writeable($dir)) { 281 | if ((is_writeable($dir . '/' . self::getFileNameFromHandle($handle))) || !file_exists($dir . '/' . self::getFileNameFromHandle($handle))) { 282 | file_put_contents($dir . '/' . self::getFileNameFromHandle($handle), $contents); 283 | 284 | return true; 285 | } else { 286 | return false; 287 | self::$errorMsg = "File $dir " . '/' . self::getFileNameFromHandle($handle) . ' can not be written to. Please check permissions'; 288 | } 289 | } else { 290 | self::$errorMsg = "Directory $dir does not exist, or is not writeable."; 291 | 292 | return false; 293 | } 294 | } else { 295 | self::$errorMsg = "Template $handle can not be found."; 296 | 297 | return false; 298 | } 299 | } 300 | 301 | /** 302 | * Writes the layout to the layout file. 303 | * 304 | * The name of the template containing the layout 305 | * @param string $handle 306 | * The layout to write to 307 | * @param string $layout 308 | * The content to write to the layout file 309 | * @param string $contents 310 | * 311 | * @return bool 312 | */ 313 | protected function _writeLayout($handle, $layout, $contents, $new = false) 314 | { 315 | if ($dir = ($new) ? EMAILTEMPLATES . '/' . $handle : dirname(self::find($handle))) { 316 | if (is_dir($dir) && is_writeable($dir)) { 317 | if ((is_writeable($dir . '/' . self::getFileNameFromLayout($layout))) || !file_exists($dir . '/' . self::getFileNameFromLayout($layout))) { 318 | file_put_contents($dir . '/' . self::getFileNameFromLayout($layout), $contents); 319 | 320 | return true; 321 | } else { 322 | self::$errorMsg = "File $dir " . '/' . self::getFileNameFromLayout($layout) . ' can not be written to. Please check permissions'; 323 | 324 | return false; 325 | } 326 | } else { 327 | self::$errorMsg = "Directory $dir does not exist, or is not writeable."; 328 | 329 | return false; 330 | } 331 | } else { 332 | self::$errorMsg = "Template $handle can not be found."; 333 | 334 | return false; 335 | } 336 | } 337 | 338 | protected function _parseConfigTemplate($handle, $config) 339 | { 340 | $default_config = array( 341 | 'datasources' => array( 342 | ), 343 | 'layouts' => array( 344 | 'html' => 'template.html.xsl', 345 | 'plain' => 'template.plain.xsl' 346 | ) 347 | ); 348 | 349 | $config = array_merge($default_config, $config); 350 | 351 | $config_template = file_get_contents(ETMDIR . '/content/templates/class.tpl'); 352 | 353 | // Author: Use the accessor function if available (Symphony 2.5) 354 | if (is_callable(array('Symphony', 'Author'))) { 355 | $author = Symphony::Author(); 356 | } else { 357 | $author = Administration::instance()->Author; 358 | } 359 | 360 | $ignore_attachment_errors = 'false'; 361 | if (isset($config['ignore-attachment-errors']) && filter_var($config['ignore-attachment-errors'], FILTER_VALIDATE_BOOLEAN)) { 362 | $ignore_attachment_errors = 'true'; 363 | } 364 | 365 | $config_template = str_replace('', self::getClassNameFromHandle(self::getHandleFromName($config['name'])), $config_template); 366 | $config_template = str_replace('', addslashes($config['name']), $config_template); 367 | $config_template = str_replace('', addslashes($config['from-name']), $config_template); 368 | $config_template = str_replace('', addslashes($config['from-email-address']), $config_template); 369 | $config_template = str_replace('', addslashes($config['reply-to-name']), $config_template); 370 | $config_template = str_replace('', addslashes($config['reply-to-email-address']), $config_template); 371 | $config_template = str_replace('', addslashes($config['attachments']), $config_template); 372 | $config_template = str_replace('', $ignore_attachment_errors, $config_template); 373 | $config_template = str_replace('', addslashes($config['recipients']), $config_template); 374 | $config_template = str_replace('', '1.0', $config_template); 375 | $config_template = str_replace('', addslashes($author->getFullName()), $config_template); 376 | $config_template = str_replace('', addslashes(URL), $config_template); 377 | $config_template = str_replace('', addslashes($author->get('email')), $config_template); 378 | $config_template = str_replace('', DateTimeObj::getGMT('c'), $config_template); 379 | $config_template = str_replace('', addslashes($config['subject']), $config_template); 380 | 381 | $datasources = ''; 382 | foreach ($config['datasources'] as $ds) { 383 | $datasources .= PHP_EOL . " '" . addslashes($ds) ."',"; 384 | } 385 | $config_template = str_replace('', $datasources, $config_template); 386 | 387 | $layouts = ''; 388 | foreach ($config['layouts'] as $tp => $lt) { 389 | $layouts .= PHP_EOL . " '$tp' => '".addslashes($lt)."',"; 390 | } 391 | $config_template = str_replace('', $layouts, $config_template); 392 | 393 | return $config_template; 394 | } 395 | } 396 | 397 | class EmailTemplateManagerException extends Exception 398 | { 399 | 400 | } 401 | -------------------------------------------------------------------------------- /lib/class.extensionpage.php: -------------------------------------------------------------------------------- 1 | _XSLTProc = new XsltProcess(); 14 | parent::__construct($params); 15 | } 16 | 17 | public function __switchboard($type = 'view') 18 | { 19 | $this->_type = $type; 20 | if (!isset($this->_context[0]) || trim($this->_context[0]) === '') $this->_function = 'index'; 21 | else $this->_function = $this->_context[0]; 22 | parent::__switchboard($type); 23 | } 24 | 25 | public function view() 26 | { 27 | $this->Contents = new XMLElement('div', null, array('id' => 'contents')); 28 | $this->Form->setAttribute('style','display:none;'); 29 | 30 | return parent::view(); 31 | } 32 | 33 | public function generate($page = null) 34 | { 35 | if ($this->_useTemplate !== false) { 36 | $template = $this->viewDir . '/' . (empty($this->_useTemplate)?$this->_getTemplate($this->_type, $this->_function):$this->_useTemplate . '.xsl'); 37 | 38 | if (file_exists($template)) { 39 | $current_path = explode(dirname($_SERVER['SCRIPT_NAME']), $_SERVER['REQUEST_URI'], 2); 40 | $current_path = '/' . ltrim(end($current_path), '/'); 41 | $upload_size_php = ini_size_to_bytes(ini_get('upload_max_filesize')); 42 | $upload_size_sym = Symphony::Configuration()->get('max_upload_size', 'admin'); 43 | $params = array( 44 | 'today' => DateTimeObj::get('Y-m-d'), 45 | 'current-time' => DateTimeObj::get('H:i'), 46 | 'this-year' => DateTimeObj::get('Y'), 47 | 'this-month' => DateTimeObj::get('m'), 48 | 'this-day' => DateTimeObj::get('d'), 49 | 'timezone' => DateTimeObj::get('P'), 50 | 'website-name' => Symphony::Configuration()->get('sitename', 'general'), 51 | 'root' => URL, 52 | 'symphony-url' => SYMPHONY_URL, 53 | 'workspace' => URL . '/workspace', 54 | 'current-page' => strtolower($this->_type) . ucfirst($this->_function), 55 | 'current-path' => $current_path, 56 | 'current-url' => URL . $current_path, 57 | 'upload-limit' => min($upload_size_php, $upload_size_sym), 58 | 'symphony-version' => Symphony::Configuration()->get('version', 'symphony'), 59 | ); 60 | $html = $this->_XSLTProc->process($this->_XML->generate(), file_get_contents($template), $params); 61 | if ($this->_XSLTProc->isErrors()) { 62 | $errstr = null; 63 | 64 | while (list($key, $val) = $this->_XSLTProc->getError()) { 65 | $errstr .= 'Line: ' . $val['line'] . ' - ' . $val['message'] . self::CRLF; 66 | } 67 | 68 | throw new SymphonyErrorPage(trim($errstr), null, 'xslt-error', array('proc' => clone $this->_XSLTProc)); 69 | } 70 | } else { 71 | Administration::instance()->errorPageNotFound(); 72 | } 73 | $this->Form = null; 74 | $this->Contents->setValue($html); 75 | } 76 | 77 | return parent::generate(); 78 | } 79 | 80 | protected function _getTemplate($type, $context) 81 | { 82 | return sprintf('%s%s.xsl', strtolower($type), ucfirst(strtolower($context))); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | All source code included in the "Email Template Manager" Symphony Extension 2 | archive is, unless otherwise specified, released under the MIT licence as 3 | follows: 4 | 5 | ----- begin license block ----- 6 | 7 | Copyright 2011-2023 Huib Keemink, Michael Eichelsdoerfer 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | 27 | ----- end license block ----- 28 | --------------------------------------------------------------------------------