Drupal powers a lot of government, university and mid-market sites. These are exactly the places where a password reset email ending up in spam is a real problem: users cannot recover their accounts, support tickets pile up, and the site gets blamed. In nearly every case the root cause is the same — Drupal is still sending mail via PHP mail() from the web host, and in 2026 that combination almost guarantees spam placement.
This article walks through the Drupal mail pipeline, why the default fails, and the two mainstream fixes: the SMTP module for a standard transactional provider, or the Mailgun module (or equivalent) for API-based sending. It ends with a seed-test you can run in twenty minutes.
Install the smtp module (or Mailgun / Sendgrid / SES module), point it at an authenticated provider, publish SPF + DKIM + DMARC for the From domain, then seed-test user registration, password reset and contact form. Each template scans differently — test them all.
How Drupal sends mail
Drupal routes all outbound mail through the mail_system service, which on a fresh install points at PhpMail. That class is a thin wrapper around PHP's mail() function. Every core module that sends — user registration, password reset, contact form, comment notifications — and every contrib module that sends — Webform, Commerce, Group — all hand off to the same service.
Swapping the mail_system implementation once swaps mail for the entire site. That is the elegant part. The ugly part is that the default implementation is PHP mail.
The default PHP mail path, failing in three ways
- Unauthenticated sending IP. PHP mail hands the message to the host's local MTA (Postfix, Exim, sendmail). It leaves from the host's IP — usually shared with many other sites.
- No SPF alignment. Your From domain's SPF record (if any) does not include the host's IP. SPF result at Gmail: softfail or none.
- No DKIM. PHP mail does not sign messages. DKIM result: none. With SPF not passing, DMARC evaluation also fails.
Gmail, Outlook and Yahoo all treat unauthenticated mail from shared hosts as low-trust by default. For transactional content (password resets, account confirmations) that trust signal matters a lot, because the recipient rarely has a prior open/reply history with your domain.
- Password reset emails "do not arrive" for Gmail users but do arrive for colleagues on the same corporate Exchange.
- New registrations stall at confirmation. Users email support.
- Webform submissions trigger notifications that land in the site-owner's Junk folder.
drush core:requirementslooks clean; the site has no errors — just silent spam.
Two fixes: SMTP module or Mailgun module
Both approaches work. Pick based on which sending provider you plan to use.
Option A: SMTP module + any SMTP provider
The contrib SMTP Authentication Support module (project name: smtp) replaces mail_system with an authenticated SMTP sender. It works with any provider that speaks SMTP.
- Install:
composer require drupal/smtp, then enable:drush en smtp. - Go to Configuration → System → SMTP Authentication Support.
- Set Turn this module on or off to
On. - Fill in the SMTP Server (e.g.
smtp.postmarkapp.com), SMTP Port (587), protocol (TLS), and username/password from your provider. - Set From Email (e.g.
noreply@yoursite.org) and From Name in the E-mail options section. - Use Send test e-mail at the bottom of the same page.
See the companion article Drupal SMTP Module + Inbox Placement for the full walkthrough with provider-specific credentials.
Option B: Provider-native module (Mailgun, SendGrid, SES)
The Mailgun, SendGrid Integration and SES Mailer modules use the provider's API instead of SMTP. Upsides: better error reporting, no open SMTP port needed, often easier for containerised hosts. Downsides: the module ties you to one provider.
- Install: e.g.
composer require drupal/mailgun, enable, and add an API key from the provider. - The module automatically takes over
mail_systemfor its keys once configured. - Test with the module's built-in test form, or from the Drupal contact form.
DNS: SPF, DKIM, DMARC
A correct transactional setup for yoursite.org looks like:
; SPF — single TXT
yoursite.org. IN TXT "v=spf1 include:mailgun.org ~all"
; DKIM — CNAME to the provider (example: Mailgun)
s1._domainkey.yoursite.org. IN CNAME s1._domainkey.mg.yoursite.org.
; Provider gives you a longer DNS record they publish on their side.
; DMARC
_dmarc.yoursite.org. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@yoursite.org; adkim=r; aspf=r"Wait for propagation, then resend the Drupal test mail to a Gmail address. Open the message, "Show original", confirmSPF: PASS, DKIM: PASS, DMARC: PASS.
A native Drupal extension is in private beta — schedule placement tests from admin and alert on drops.
Seed-test the real templates
The module test email is trivial. Your real Drupal templates — user registration, password reset, contact form notification, Webform results — scan differently.
- Generate 20+ seed addresses at check.live-direct-marketing.online covering Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, Web.de and ProtonMail.
- Register a new account using the first seed. Watch the confirmation email fan out to the panel, with per-provider placement.
- Trigger a password reset. Different content, different result — test separately.
- Submit the contact form from a seed. Check how the notification lands at the admin mailbox.
Aim for 95%+ inbox across consumer providers. Below 80% and something is still wrong — usually DNS alignment or a DKIM selector mismatch.