Programmatic Sending
When to Use
Use
MailManager::mail()+hook_mail()when custom code needs to send transactional email — order confirmations, invitations, password resets, onboarding sequences, payment receipts. This approach ensures the routing layer applies.
Decision
| Approach | Use When |
|---|---|
MailManager::mail() + hook_mail() |
Default — works with system.mail.yml routing, supports Mailgun-specific params |
| EmailBuilder plugin (Mailer Plus) | Using Mailer Plus; want Twig templates and policies |
| Direct Mailgun SDK call | Almost never — bypasses routing, mocks, test mode, queue |
Pattern
Step 1 — Implement hook_mail() in your module
// my_module.module
function my_module_mail($key, &$message, $params) {
switch ($key) {
case 'welcome':
$message['subject'] = t('Welcome to @site', ['@site' => $params['site_name']]);
$message['body'][] = $params['body'];
$message['headers']['Content-Type'] = 'text/html; charset=UTF-8';
// Pass Mailgun extras through.
foreach (['tags', 'tracking', 'tracking_clicks', 'tracking_opens', 'reply-to', 'bcc'] as $k) {
if (isset($params[$k])) {
$message['params'][$k] = $params[$k];
}
}
break;
}
}
Step 2 — Send from a service
// Inject mail.manager into your service:
// arguments: ['@plugin.manager.mail']
public function sendWelcome(UserInterface $user): bool {
$params = [
'site_name' => \Drupal::config('system.site')->get('name'),
'body' => '<p>Hi <strong>' . $user->getDisplayName() . '</strong>, welcome!</p>',
'tags' => ['welcome', 'transactional'],
'tracking' => TRUE,
];
$result = $this->mailManager->mail(
'my_module', 'welcome', $user->getEmail(),
$user->getPreferredLangcode(), $params, NULL, TRUE
);
return !empty($result['result']);
}
HTML emails
$message['headers']['Content-Type'] = 'text/html; charset=UTF-8';
$message['body'][] = '<p>HTML content here</p>';
For Twig-themed HTML, use Mailer Plus — it ships with template suggestions and *.libraries.yml-based CSS injection.
Attachments
$params['attachments'] = [
[
'filepath' => '/sites/default/files/private/invoice-' . $invoice_id . '.pdf',
'filename' => 'invoice.pdf',
'filemime' => 'application/pdf',
],
];
Attachments are counted against Mailgun's 25 MB per-message limit.
Common Mistakes
- Wrong: Calling Mailgun's PHP SDK directly from a controller → Right: Use
MailManager::mail()so routing, mailsystem, queue, and test mode all apply. - Wrong: Putting subject/body/headers in
$params→ Right: Set them on$messageinhook_mail().$paramsis for arbitrary pass-through data. - Wrong: Forgetting
Content-Type: text/htmlfor HTML emails → Right: Without it, Mailgun receives HTML as plain text and recipients see raw markup. - Wrong: Hardcoding the From address in
hook_mail()→ Right: Let the Mailgun module's "From email" default apply.