Skip to content
Developer docs

Sending email from apps

3 min read

Apps send email through the site's own settings with $this->api()->mail(), never their own SMTP connection, so their messages come from the site's address, use its template and pass the same checks as core's. This page covers the mail API, its hooks and the rules for sending from an app. Site owners setting up email want How to set up email so your site's messages arrive.

Sending

$mail = $this->api()->mail();

// In the site's template: site name, heading, your HTML, footer.
$ok = $mail->sendTemplate('owner@example.org', 'New booking', 'You have a new booking',
    '<p>Anna booked <strong>Studio A</strong> for Friday at 10:00.</p>');

// Your own HTML. A plain-text part is made from it unless you pass 'text'.
$ok = $mail->send(['a@example.org', 'b@example.org'], 'Weekly report', $html, [
    'reply_to'  => 'support@example.org',
    'from_name' => 'Northwind bookings',
]);

if (!$ok) {
    $this->log('Booking email failed: ' . $mail->lastError(), [], 'warning');
}
MethodReturns
send($to, $subject, $html, $options)true when the server accepted it. $to is one address or a list. Options: text, from, from_name, reply_to, headers (a list of Name: value lines)
sendTemplate($to, $subject, $heading, $bodyHtml, $footNote)the same, in the site's template; $footNote replaces the footer line
lastError()why the last send failed, or ''
isConfigured()whether the site has a sender address
  • Permission. An app that declares permissions must declare mail.send, and check it before optional mail: if ($this->allowed('mail.send')) { … }. Without it, sends return false and are logged.
  • Escape anything people typed before putting it in the HTML: htmlspecialchars($name, ENT_QUOTES, 'UTF-8').
  • Send outside transactions. api()->transaction() may run its function again; mail sent inside would go twice.
  • Many messages? Each send waits for the server. Send in small batches from a scheduled task, and stay within your provider's limits.
  • Don't change the From address to one off the site's domain unless your SMTP account may send as it; set reply_to instead.

Hooks

HookUse
mail.before_send (filter)Receives the message as an array: to, subject, html, text, from, from_name, reply_to, headers. Return it changed, or false to stop it.
mail.sent (action: $mail)After the server accepted a message.
mail.failed (action: $mail, $error)After a failure, with the reason.
// A staging site: deliver everything to one inbox, and mark it.
$this->addFilter('mail.before_send', function (array $mail) {
    $mail['subject'] = '[staging] ' . $mail['subject'] . ' (for ' . implode(', ', $mail['to']) . ')';
    $mail['to'] = ['dev-inbox@example.org'];
    return $mail;
});

// Keep a record of failures in your app's log.
$this->addAction('mail.failed', function (array $mail, string $error) {
    $this->log('Mail to ' . implode(', ', $mail['to']) . ' failed: ' . $error, [], 'warning');
}, 10, 2);

How core sends it

  • Two methods, chosen under Settings → Email: PHP mail(), with -f set to the From address when it is on the site's domain, or SMTP with STARTTLS or SSL, AUTH LOGIN and a 15-second timeout per step.
  • The default From address is noreply@ the domain of APP_URL, without www..
  • There is no queue: a send happens during the request and isn't retried. Failures are logged as ERROR: Mail send failed: … (to: …), and fire mail.failed.
  • The SMTP password is redacted from the REST API and MCP settings, whatever the key's scopes.

Discussion

Leave a comment