Skip to content

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.

See also