Skip to content
V
For developers
Architecture, conventions, the core and security
Core / Sending e-mail

Sending emails#

In this system an email is never sent directly from a presenter. A manager calls one of three methods from SendEmailTrait, that method composes the message and — depending on a setting — either queues it or sends it immediately. The queue is then dispatched by a cron.

The code: app/Core/Traits/Api/System/Models/Managers/SendEmailTrait.php (the Api variant) and app/Core/Traits/Shared/System/Models/Managers/SendEmailTrait.php (the shared one).

Three methods#

Method When to use it
sendEmail($langId, $emailKey, $to, $values, …) the default choice — a template from the database by key, in the recipient's language
sendEmailFromText($subject, $body, $from, $to, $values, …) text composed in code; no template exists in the database
sendEmailToAll($emailKey, $recipients, $values, …) sending the same template to several recipients

All of them take $values (placeholders for the template), $files (attachments) and $replyTos.

A reply-to address is useful where the recipient should write to someone other than the system

Typically for a reply to an advert: the message is sent by the system, but the answer should go to the advertiser.

Where the message goes: the 1/emailSending setting#

This is the single place deciding between the queue and immediate sending:

Value Behaviour
1 everything into the queue (the default)
2 into the queue only for non-preferred messages; preferred ones go out immediately
anything else everything is sent immediately, the queue is not used

Why a queue at all

Sending takes seconds. An order sending two emails would take noticeably longer to process — and during a mail server outage it would fail entirely. This way only the message waits.

The preferred flag only matters in mode 2

The preferred flag distinguishes a registration confirmation or a password reset from a bulk mailing. With emailSending: 1 even those go into the queue — and they overtake the newsletter there, not before it.

The queue#

The EmailQueue entity carries the recipient, the sender, the subject, the body, the attachments, the reply-to addresses and a set of operational fields:

Field Meaning
sent, sendDatetime sent and when
attempts the number of attempts
errorLog the last error
preferred priority over bulk mailing
stopped stopped by hand — the message will not be retried
newsletter a link to the mailing when it is a newsletter

It is dispatched by the /cron/system/email-queue/send cron — every minute.

Without the cron running, nothing goes out and nobody is told

The queue only grows. The user sees no error, because from the application's point of view the send succeeded — the message was stored, after all. After a deployment verify that the task runs, and then verify it again.

The newsletter is deliberately slow

Thousands of messages within a second look like spam to mail servers. Batching protects the domain's reputation; it is not an unfinished feature.

Templates#

Templates live in the database, keyed by a string, with a text per language and placeholders replaced from $values.

A template key is never renamed

The code looks the template up by exactly that key (findBy([new EqualFilter(['key'], [$emailKey])])). After a rename sendEmail() does not find it and returns a failure — and because the return value often goes unchecked, the message simply stops arriving.

A missing template translation means an email in another language, not an error

The text is looked up for the recipient's $langId. If there is none, the fallback applies — the recipient gets the message in the main language.

Procedure: adding a new email#

  1. Create the template in the administration (System → E-mails) and give it a key that does not exist yet.
  2. Fill in the text for every language — not just the main one.
  3. Call sendEmail() from a manager, not from a presenter:
$this->sendEmail(
    langId: $user->getLanguageId(),
    emailKey: 'order.confirmation',
    toEmailAddrr: $order->getEmail(),
    values: ['orderNumber' => $order->getNumber()],
);
  1. Call it from completedSave / completedDelete, that is, after the commit.
  2. Evaluate the result — a failure means the message was not even queued.
  3. Verify on the development machine where the message actually went (see below).

An email in afterSave goes out even when the transaction is rolled back

afterSave runs inside the transaction. The message about a created order is sent, the order is rolled back — and the customer holds a confirmation of something that does not exist. Irreversible effects belong after the commit, see Manager lifecycle.

Fields are NOT filtered by audience in a cron

The dispatch runs outside a request, so #[FieldAccess] does not apply — deliberately, since an activation email needs activationKey. But it means what ends up in the message body is the responsibility of the template's author and of $values.

The development environment#

Dev and production share a database

A test mailing can reach real customers. Before sending anything, check the value of 1/emailSending and the contents of the queue — and for a bulk mailing check the recipient list too.

Where to look#

I want Where
the three sending methods app/Core/Traits/Api/System/Models/Managers/SendEmailTrait.php
the queue-versus-immediate decision app/Core/Traits/Shared/System/Models/Managers/SendEmailTrait.php
the queue entity app/Core/Base/Shared/System/Models/Entities/EmailQueue.php
the dispatch cron app/UI/Cron/System/Presenters/EmailQueuePresenter.php
managing the templates the administration → System → E-mails

Follow-up chapters: Manager lifecycle · Cron presenters · Scheduled tasks · Translations