DACore · coming December 2026
How DACore email senders and templates work
DACore now owns outgoing mail the same way it owns rights and the sidebar.
An operator creates SMTP accounts and HTML templates once, on the DACore desk.
Every other module then picks a sender (and a template) in its own settings and calls
DotApp::call('DACore:Email@…!') — no HTTP route, no CRC, no second mailer table.
Shop does not learn SMTP. Shop remembers which DACore sender to use.
Common mistakes
| Wrong | Right |
|---|---|
A shop_smtp table and Email::send('main', …) from app/parts. |
DACore:Email@listSenders! in settings, then Email@send! on the event. |
INSERT into dacore_email_senders / dacore_email_templates. |
registerSender! / addTemplate!, or the DACore screens. |
'id' => 7 on Email@send!. |
Send wants the encrypted token from listSenders or registerSender. A raw number is rejected. |
Both template and text, or neither. |
Exactly one body source. |
{{ var: $name }} inside a DACore mail body. |
{{ name }} — mail templates are not Renderer views. |
Overwrite TESTMAIL / CONFIRM / WELCOME from the installer. |
Those slugs are protected. Use Shop.OrderPaid. |
Put a production mailbox password in Installation.php. |
The operator creates SMTP in DACore. The installer only seeds your templates. |
What the operator does in DACore
Root opens two screens under the admin prefix: {prefix}/dacore/email-senders and {prefix}/dacore/email-templates.
They are ordinary DACore pages: list, search, AJAX pager, create, edit. Other modules do not post to those URLs.
Senders are SMTP accounts: display name, From address, host, port, TLS / SSL / none, username, password, timeout, and an optional default flag.
The host is stored as a bare hostname — ssl://mail.example.com/ is stripped to mail.example.com.
Suggested ports in the UI: 25 (none), 587 (TLS), 465 (SSL).
The mailbox password is encrypted with the application key, not the session key, so a new login still sends.
The first saved sender becomes the default. Marking another default clears the previous one.
Edit a sender and you can send a test message. That uses the system TESTMAIL template.
Leave the recipient empty and DACore writes to the sender’s own From address.
A failed test returns the transport errors (password redacted). A wrong TLS/SSL versus port pairing gets a short hint: TLS on 587, SSL on 465.
Templates are HTML fragments with {{ name }} placeholders.
The editor starts in visual mode and can switch to HTML source.
System templates cannot be deleted. An operator may still edit their HTML so the product wording can change without a code deploy.
System templates
DACore seeds three protected slugs. addTemplate! refuses to create or overwrite them (the check is case-insensitive).
| Slug | Who uses it |
|---|---|
TESTMAIL |
Email@testSender! and the Test button on the sender form. |
CONFIRM |
Account email confirmation. Pass confirm_link in vars when you send it. |
WELCOME |
Welcome mail after a confirmed account. |
Your module may send CONFIRM or WELCOME by slug. It must not replace them.
Module-owned mail uses {Module}.{Purpose} — Shop.OrderPaid, Crm.Reset.
The module API
Same calling style as rights. In-process only. Check every return. The helpers never throw to you — the controller logs and returns a failure shape.
| Call | Returns |
|---|---|
DACore:Email@listSenders! |
Rows: id, token, name, email, is_default (0/1). No password, no host. |
DACore:Email@listTemplates! |
Rows: id, token, slug, name, is_system (0/1). |
DACore:Email@registerSender! |
{ok, id, token} or {ok: false, message, errors}. Upsert by name. |
DACore:Email@testSender! |
{ok, message, errors}. Sender may be id, token, or name. |
DACore:Email@addTemplate! |
{ok, id, slug} or a failure shape. Upsert by slug. |
DACore:Email@send! |
true or a list of error strings. Not {ok}. Test with !== true. |
DACore:Email@senderIdForApi! |
Positive int, or 0. Resolves a token, a numeric id, or a sender name. |
Settings in your module: pick a sender
This is the whole point. Your settings screen is a select. DACore already holds the SMTP secret.
$senders = DotApp::call('DACore:Email@listSenders!');
if (!is_array($senders) || $senders === []) {
// Product copy: add an SMTP account in DACore → Email senders
}
In the HTML, the option value is token. Never put the integer id in the page.
id on the row is for PHP only — matching the stored setting after a new encrypt.
Tokens are not stable across renders; decrypt still works, so a token you saved from an earlier POST still sends.
On save, resolve the posted token to an integer and keep that in shop_* settings (never echo it):
$token = (string) ($data['mail_sender'] ?? '');
$senderId = (int) DotApp::call('DACore:Email@senderIdForApi!', $token);
if ($senderId < 1) {
return /* invalid sender */;
}
// persist $senderId in your module settings
On the next GET, call listSenders! again. Mark selected where (int) $row['id'] === $storedId.
Option values stay tokens. Empty list: tell the operator to create a sender in DACore. Root can use {prefix}/dacore/email-senders.
Templates work the same way with listTemplates!. Prefer persisting the slug (Shop.OrderPaid) — it is stable and is what Email@send! accepts as template.
Seed templates at install
If Shop needs an order-paid letter, add it in Installation.php next to rights and menu.
Second install updates name and body for that slug. It does not duplicate the row.
$tpl = DotApp::call('DACore:Email@addTemplate!', [
'slug' => 'Shop.OrderPaid',
'name' => 'Order paid',
'body' => '<p>Hi {{ name }}, order {{ order_no }} is paid.</p>',
]);
if (($tpl['ok'] ?? false) !== true) {
Logger::use()->error('Shop template failed', ['errors' => $tpl['errors'] ?? []]);
}
Slug rules: start with a letter or digit, then letters, digits, dot, underscore, hyphen. Max 64.
name max 128. Body must be non-empty HTML.
A later install overwrites that slug even if an operator edited the HTML in DACore.
If you must keep operator edits, skip addTemplate! when listTemplates! already contains the slug.
Do not send mail from the installer. Do not register a production SMTP account there unless you are shipping a lab mailbox on purpose.
Uninstall must not DELETE DACore senders — they are shared. Leaving your templates is fine; the operator may still use them.
Send
$senders = DotApp::call('DACore:Email@listSenders!');
$token = '';
foreach (is_array($senders) ? $senders : [] as $row) {
if ((int) ($row['id'] ?? 0) === $storedSenderId) {
$token = (string) ($row['token'] ?? '');
break;
}
}
$result = DotApp::call('DACore:Email@send!', [
'id' => $token,
'to' => $customerEmail, // string, CSV, or string[]
'subject' => 'Your order',
'template' => 'Shop.OrderPaid', // slug (or a template token)
'vars' => [
'name' => $customerName,
'order_no' => $orderNo,
],
]);
if ($result !== true) {
Logger::use()->error('Shop mail failed', ['errors' => $result]);
}
id is the sender token. A numeric sender id is rejected on purpose (the unit test locks that).
testSender! is more tolerant: id, token, or display name.
to / cc / bcc accept one address, a CSV string, or an array. A bad address fails the whole send.
subject is required; newlines are stripped.
template xor text — not both, not neither.
attachments is a list of absolute paths; missing files are skipped.
Default body type is HTML. Set html => false or contentType => 'text/plain' for plain text.
Register and test (optional)
Everyday SMTP is created on the DACore screen. registerSender! is for a wizard, a lab mailbox, or an update of an account your module already named.
Same name, second call: the row is updated, not duplicated. Empty password on update keeps the stored secret.
$sender = DotApp::call('DACore:Email@registerSender!', [
'name' => 'Shop mail',
'email' => 'shop@example.com',
'host' => 'smtp.example.com',
'port' => 587,
'secure' => 'tls', // '' | tls | ssl
'username' => 'shop@example.com',
'password' => $secret,
'timeout' => 30,
'is_default' => '1',
]);
if (($sender['ok'] ?? false) !== true) {
Logger::use()->error('Shop sender failed', ['errors' => $sender['errors'] ?? []]);
return;
}
$probe = DotApp::call('DACore:Email@testSender!', $sender['token'], 'you@example.com');
// also: $sender['id'], 'Shop mail', or ['id' => $sender['token'], 'to' => 'you@example.com']
Placeholders
Mail bodies are not Renderer layouts. The token is {{ key }} (spaces around the key are allowed). Keys match lowercase [a-z0-9_]+.
Values from vars are HTML-escaped. Unknown tokens stay in the HTML.
| Token | Source |
|---|---|
{{ name }} |
Your vars, or the built-in fallback (email if name is empty). |
{{ email }} |
Built-in context — pass it in vars when you need it. |
{{ app_name }} |
DACore template.loginsystemname. |
{{ date }} {{ time }} {{ year }} |
Server clock. |
{{ confirm_link }} |
Built-in; pass the URL in vars for confirm mail. |
{{ order_no }} |
Any extra key you pass in vars. |
Do not put secrets in vars. Do not invent {{ var: $order_no }} — that syntax belongs to views, not to DACore mail.
What this is not
It is not the framework Email facade in app/parts. That API still exists for a site that has no DACore.
A module that lives under the admin desk must not open a second SMTP stack.
It is not an HTTP mail endpoint. Do not wrap Email@send! in a public route.
It is not your inbox. Operator alerts stay on DACore:Notifications@push.
FAQ
Who can open the DACore mail screens?
Root only (dotapp.root). Your module settings page uses your own rights. The in-process API has no HTTP gate — call it from trusted PHP, after your own Auth::can().
What if the operator never picks a sender?
listSenders! puts the default first (is_default = 1). You may preselect that row. You must still persist an explicit choice before you send, or fail with product copy when the list is empty.
Can I pass the sender name to Email@send?
Not today. send decrypts id as a token. Resolve name or stored id through listSenders! (or senderIdForApi! plus a fresh token) and pass that token.
Can I still use Config::email and Email::send?
On a public site without DACore, yes — that is AIRULES doc 21. Under DACore, use this API so every module shares the accounts the operator already tested.
Is this the same as navbar notifications?
No. Notifications are the in-app inbox. Email is SMTP. An “order paid” event can do both: Notifications@push for the night operator, Email@send! for the customer.