NTF-002
Transactional email templates worth sending
A tiny template layer with placeholder substitution, stored in the database so an operator can rewrite the wording without touching code.
Transactional mail is a handful of messages: verify this address, reset this password, your payout was sent, your payout failed. Hardcoding them means an operator who wants to change a sentence has to edit PHP.
Storing them as rows with {placeholder} tokens makes them editable, translatable, and testable. The substitution is one strtr call.
Escape placeholder values in the HTML version. A username is user-supplied text and it is going into markup.
function render_template(PDO $pdo, string $key, array $vars): ?array
{
$st = $pdo->prepare('SELECT subject, body_text, body_html FROM mail_templates WHERE tpl_key = ? AND active = 1');
$st->execute([$key]);
$tpl = $st->fetch(PDO::FETCH_ASSOC);
if (!$tpl) {
return null;
}
$plain = [];
$html = [];
foreach ($vars as $k => $v) {
$plain['{' . $k . '}'] = (string) $v;
$html['{' . $k . '}'] = htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
}
return [
'subject' => strtr((string) $tpl['subject'], $plain),
'text' => strtr((string) $tpl['body_text'], $plain),
'html' => $tpl['body_html'] ? strtr((string) $tpl['body_html'], $html) : '',
];
}
/*
INSERT INTO mail_templates (tpl_key, subject, body_text, active) VALUES
('verify', 'Confirm your {site} account',
"Hello {username},\n\nConfirm your address to finish signing up:\n{link}\n\n"
. "The link works for 24 hours. If you did not sign up, ignore this.\n\n{site}", 1),
('payout_sent', 'Your {site} payout is on its way',
"Hello {username},\n\n{amount} {currency} has been sent to {address}.\n"
. "Transaction: {txid}\n\n{site}", 1),
('payout_held', 'Your {site} payout needs a look',
"Hello {username},\n\nYour withdrawal of {amount} {currency} is on hold while we check it.\n"
. "Nothing has been lost — your balance is safe and we will finish it shortly.\n\n{site}", 1);
*/
Using it
Always include the site name and a plain explanation of why the message was sent. Transactional mail that does not say what triggered it gets reported as spam.
Send the held-payout message. Silence on a delayed withdrawal is what turns a technical hiccup into an accusation.
Keep a preview in admin that renders a template with sample values, so wording changes can be checked without sending.
What bites people
Escape into HTML, not into text. Running htmlspecialchars over the plain text version puts & in front of users.
A missing placeholder leaves {username} in the message. Validate that every token in a template has a value before sending.
Do not put balances or wallet addresses in mail unless necessary. Mail is not private and often forwarded.