Writing an Email Template

4 views Markdown

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

a group that has a resolver Put the event in an existing group whenever you can. A new group needs a resolver and an entry in the resolver map. A group without one answers every dispatch with an error.
the engine setting Templates are written for the configured engine, and the shipped tree is Smarty. Check options/notification-template-engine before you copy placeholder syntax from somewhere else.
every installed language There is no fallback between languages. Ship the trio in each language the installation has, or the recipients reading in the missing one get nothing at all.
a way to read what was sent A sandbox mail module writes each outgoing message to disk instead of opening a connection. It is the fastest way to inspect a body, and the only safe one on an installation with real addresses in it.

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.

what surrounds your file
{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
rows, not a page Begin with a table row and end with one. No document tag, no head, no body tag, and no stylesheet: the frame already opened all of them.
presentation goes inline Mail clients drop stylesheets, so every shipped body carries its styling as an attribute on each element. Tables are used for layout for the same reason.
colours come from the installation Three are injected: 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.
logic can hide in comments The shipped templates wrap loops and conditions in HTML comments so a visual editor does not mangle them. The engine still executes them; only the comment markers survive into the output.

The Three Files

FileRead forContains
{name}.htmlmailThe body fragment. Wrapped in the shell before sending.
{name}.jsonmail{"subject": "..."}. Built with the same variables, so it may carry placeholders.
{name}.txttext messageThe 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

  1. Pick the group and a lower-case, dash-separated event name. The pair is the identity of the event everywhere else.
  2. Create {name}.html, {name}.json and {name}.txt under every installed language directory.
  3. Copy the closest shipped body rather than writing table markup from scratch. The spacing and the colour variables are already correct there.
  4. 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

  1. Add an entry under the group in coremio/configuration/notifications.php. The keys are listed in the reference below.
  2. Set status to 1 and turn on the channels the event actually uses. Everything left at 0 stays silent.
  3. 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

  1. Open the group's resolver in coremio/helpers/notification.php and add a case for the event name.
  2. 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.
  3. 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.
  4. Call the dispatch and read the returned status. An error here means the resolver returned nothing, usually because the entity could not be loaded.

4. Decide When It Goes

  1. By default the message is queued and leaves within a minute.
  2. An event the customer is waiting on (a code, a link, an invitation) is delivered inline instead. Add its name to the $sync_notifications list, in the same file as the resolver.
  3. A single call can override that list without changing it: pass '_sync' => true in the context. Use the list for an event that is always urgent, the context key for one caller that is.
  4. 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

  1. 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.
  2. 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.
  3. Open the preview. It is also the only place a compile error is shown rather than logged.

6. Verify It

  1. Trigger the real flow, not the preview.
  2. Read the delivered message from the sandbox mail module's output directory and check that no placeholder survived into the text.
  3. 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.

coremio/configuration/notifications.php
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,
            ],
        ],
    ],
];
status The master switch. 0, or a missing entry, makes every dispatch answer disabled without touching the template at all.
user-mail · user-sms Whether the account holder is written to on that channel. A text message also needs a mobile number on the account; without one the recipient drops out of the list.
admin-mail · admin-sms Whether staff are written to as well. The recipients are resolved from the departments below, plus the addresses in the two fields after them. They fall back to the root administrator when the switch is on but nothing resolved.
emails · phones Comma separated extra staff recipients, added on top of the departments. Read only when the matching admin switch is on.
departments Support department ids whose staff receive the staff copy. An array of id strings, and the usual way to route an event to the right team instead of to everyone.
user-notification Whether the recipient also gets an in-app row. Absent means on, which is why existing events keep their bell after a new key is introduced.
admin-notification Whether staff get an in-app copy. When the key is absent the answer is derived. It is on for a short list of events staff must act on, and otherwise follows admin-mail.
send-pdf Invoice group only. Absent means on, so an invoice mail attaches the generated document unless the entry says otherwise.
variables Editor metadata, nothing more. It fills the badge list the operator sees while editing and is never consulted when a message is built. An out-of-date list misleads the operator but breaks nothing.

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.

coremio/classes/View.php
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
the return is the failure signal An empty array means the file was not found or was empty. There is no exception and no log line, so a caller that does not check it sends nothing and reports success.
what gets added on top Logos, colours, site title, company details, contact link, current year, and the whole recipient profile when a user id was passed. Your own variables win over none of these, so do not reuse their names.
building is not sending The call returns a finished body; nothing has left the installation. Hand the result to the queue, or to the low-level sender, depending on whether it may wait.

Example

A complete event, in the order the platform reads it.

templates/notifications/content/en/service/acme-quota-reached.json
{"subject":"{$service_name} has reached {$quota_percent}% of its quota"}
templates/notifications/themes/aurora/en/service/acme-quota-reached.html (styling attributes left out for readability)
<!-- 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>
the resolver case that supplies the one variable the group does not
// 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;
}
the call, and reading the outcome
// '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

Nothing is escaped for you

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.

Some variables are real credentials

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.

A preview that looks right proves less than it seems

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.

One event, several bodies

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.