Writing an Email Template
Add a new mail to the platform by writing three small files and registering them in four other places. None of those steps can be skipped.
Overview
What you author for a mail is a fragment and a subject line. The document around it, the logo, the footer, the contact block and the colours all come from the shared shell. A template that opens its own document tag fights the frame.
The rest of the work is registration. Without a configuration entry the event has no switch and is reported as disabled. Without a resolver case the variables the body prints do not exist, and without a label the panel lists the raw key.
Prerequisites
options/notification-template-engine before you copy placeholder syntax from somewhere else.
Structure
The Fragment Contract
The shell's header opens the outer table and leaves it open; the footer closes it. Your body sits between the two, which fixes what it may begin and end with.
{lang}/header.html opens the page table, prints the logo and the site title, leaves it OPEN
↓
{lang}/content.html a single {$notifi_body} placeholder
↓
YOUR FILE one or more table rows: it continues the open table
↓
{lang}/footer.html the contact block, the links, the company details, then CLOSES everything
theme_color1, theme_color2 and theme_text_color. Each holds the digits without the leading marker, so a template writes the marker itself and the value after it: bgcolor="#{$theme_color1}". Never hard-code a brand colour.
The Three Files
| File | Read for | Contains |
|---|---|---|
{name}.html | The body fragment. Wrapped in the shell before sending. | |
{name}.json | {"subject": "..."}. Built with the same variables, so it may carry placeholders. | |
{name}.txt | text message | The text body, sent with no shell. Required even if the event never uses the text channel, because the file is what the panel edits. |
Walkthrough
1. Write the Trio
- Pick the group and a lower-case, dash-separated event name. The pair is the identity of the event everywhere else.
- Create
{name}.html,{name}.jsonand{name}.txtunder every installed language directory. - Copy the closest shipped body rather than writing table markup from scratch. The spacing and the colour variables are already correct there.
- Keep the placeholders in the engine's syntax. A placeholder written in another engine's syntax is not an error you will see. It is a compile failure that leaves the entire file unrendered.
2. Register the Event
- Add an entry under the group in
coremio/configuration/notifications.php. The keys are listed in the reference below. - Set
statusto 1 and turn on the channels the event actually uses. Everything left at 0 stays silent. - Reload and confirm the event now appears in the panel's template list. If it does not, the entry is under the wrong group key.
3. Feed the Variables
- Open the group's resolver in
coremio/helpers/notification.phpand add a case for the event name. - Set only what the group's baseline does not already provide. The invoice, service, order, domain and ticket resolvers each build a full set before the switch runs.
- If the mail must reach an address that is not the account's, override the recipient inside the case. Doing it there rather than at the call site keeps every existing caller behaving as before.
- Call the dispatch and read the returned
status. Anerrorhere means the resolver returned nothing, usually because the entity could not be loaded.
4. Decide When It Goes
- By default the message is queued and leaves within a minute.
- An event the customer is waiting on (a code, a link, an invitation) is delivered inline instead. Add its name to the
$sync_notificationslist, in the same file as the resolver. - A single call can override that list without changing it: pass
'_sync' => truein the context. Use the list for an event that is always urgent, the context key for one caller that is. - Inline delivery happens in the request, so it also fails in the request. Only put an event there when the delay is worse than the risk.
5. Make It Previewable and Named
- Add a human label for the event key to the admin notification language files, in every language. Without it the panel prints the raw key in its list.
- Add sample values for the event's own variables to the preview operation in
coremio/operations/AdminNotifications.php. The preview does not run the resolver, so anything it is not given comes out as an empty string. - Open the preview. It is also the only place a compile error is shown rather than logged.
6. Verify It
- Trigger the real flow, not the preview.
- Read the delivered message from the sandbox mail module's output directory and check that no placeholder survived into the text.
- Repeat with an account in the other language. That catches a trio you created in only one language directory.
Reference
The Configuration Entry
One array per event, under its group. Only status and the four channel switches decide whether anything is sent. The rest shape who receives it and what the panel shows.
return [
'notifications' => [
'invoice' => [
'invoice-created' => [
'variables' => '{invoice_idn},{invoice_total},{invoice_payment_link},{items}',
'emails' => '',
'phones' => null,
'departments' => ['4'],
'status' => 1,
'user-mail' => 1,
'admin-mail' => 0,
'user-sms' => 1,
'admin-sms' => 0,
],
],
],
];
disabled without touching the template at all.
admin-mail.
Building a Message Directly
The dispatch calls this once per recipient. Call it yourself only when there is no dispatch to make. That means a raw address list, or anything where you already hold the variables.
public static function notifications(
$type = 'mail', // 'mail' | 'email' (alias) | 'sms' — picks .html or .txt
$template_name = '', // "group/name", no extension
$content = '', // pass a body to render THAT instead of reading the file
$variables = [], // your variables; the platform's are added on top
$lang = '', // empty falls back to the currently selected language
$user = 0 // a user id fills every user_* variable from the account
): array; // ['content' => ..., 'subject' => ...]; EMPTY array on failure
Example
A complete event, in the order the platform reads it.
{"subject":"{$service_name} has reached {$quota_percent}% of its quota"}
<!-- the shell left the outer table open: continue it, do not reopen it -->
<tbody>
<tr>
<td>
<p>Dear <strong>{$user_greeting_name}</strong>,</p>
<p>{$service_name} has used {$quota_percent}% of its allowance on {$service_domain}.</p>
</td>
</tr>
<!-- Conditions and loops are wrapped in comments so a visual editor leaves them alone.
The engine executes them anyway; only the comment markers reach the recipient. -->
<!-- {if $quota_percent >= 100} -->
<tr>
<td><p>New uploads are refused until the allowance is raised.</p></td>
</tr>
<!-- {/if} -->
<tr>
<td>
<!-- theme_color1 holds the digits only, so the marker is written here.
A button is a nested table with bgcolor: mail clients drop CSS backgrounds. -->
<table border="0" cellpadding="0" cellspacing="0" bgcolor="#{$theme_color1}">
<tbody><tr>
<td align="center"><a href="{$service_detail_link}">Open the service</a></td>
</tr></tbody>
</table>
</td>
</tr>
</tbody>
// coremio/helpers/notification.php, inside resolve_service_context().
// service_name, service_domain and service_detail_link are already built above
// the switch; only what is specific to this event belongs inside it.
switch ($name) {
case 'acme-quota-reached':
$variables['quota_percent'] = (int) ($context['percent'] ?? 0);
break;
}
// 'entity' is what every resolver accepts; each group also takes its own alias
// ('invoice', 'service', 'order', 'ticket'). A row or an id both work.
$result = \Notification::dispatch('service', 'acme-quota-reached', [
'entity' => $service,
'percent' => 92,
]);
// Never treat the return as a boolean. 'disabled' means the operator turned the
// event off and is not a failure; 'error' means the resolver could not build it.
if (($result['status'] ?? '') === 'error')
Logger::getInstance()->warning('quota notice not built', [
'service_id' => $service['id'],
'message' => $result['message'] ?? '',
]);
Pitfalls
This engine prints values exactly as they arrive, markup included, unlike the theme engine which escapes by default. Anything a customer or a third party typed needs the escape modifier before it reaches a mail body. A ticket message above all.
The service variable set includes the decrypted account password and the server login. Printing it puts a working credential in an inbox and in the mail log forever. Link to the service page instead, and reserve the credential for the one message whose entire purpose is delivering it.
The preview builds its own sample values and never calls the resolver. A body full of variables nobody supplies still looks perfect there. Only a real dispatch proves the wiring.
The template is read once per recipient, in that recipient's own language. A staff copy of a customer event comes from the same file with the same wording. Avoid a sentence that only makes sense addressed to the customer.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.