Skip to content

Powered by Grav CMS + Helios Theme

Health

Newsletters → Health checks email deliverability. It runs nine checks on your sending domain, mail transport and the last thirty days of sending. Each result explains the finding and what to do next.

Use this page to investigate reports that your emails are going to spam, and to check your setup before the first campaign.

Nothing on the screen changes anything. It reads DNS, reads configuration and counts rows. The fixes are all somewhere else (your DNS provider, the Email plugin's settings, your transport plugin's settings, this plugin's settings), and each check says which.

Why it matters

A store can look healthy from the inside and still land in spam. The receiving mail server decides using four facts the store never sees: whether your domain says the transport may send as you, whether the messages are signed, whether you have told receivers what to do about forgeries, and how the last month of your mail behaved.

Since February 2024 Gmail and Yahoo both require SPF, DKIM and DMARC of anybody sending them mail in volume, both require a working one-click unsubscribe header, and Gmail filters you past a spam complaint rate of 0.3%.

Run the checks

  1. Go to Newsletters → Health.
  2. Read down the page. The checks are grouped by whose problem each one is, in the order worth looking, so start with the first red row.
  3. Fix whatever the first failing row names.
  4. Click Check again.

The domain lookups are cached for ten minutes. Check again empties that cache and does the lookups for real. Press it after changing a record at a registrar.

A row saying Still checking on a first visit is a lookup that had not come back inside the screen's two-second budget. It fills itself in a moment later.

Your domain

Check What it reads What a failure means
SPF The TXT records on your sending domain There is no v=spf1 record; or there are two, which receivers treat as none; or the record does not let your transport send for you; or it costs a receiver more than ten DNS lookups, past which the whole thing is a permanent error
DKIM <selector>._domainkey.<domain> There is no signing key at the selector, or it points into a different provider's zone, usually a store that changed transport and left the record behind
DMARC _dmarc.<domain> There is no record, there are two, or the record has no policy in it. A policy of none is a warning rather than a failure
From address This plugin's own from address, or the Email plugin's It is not set, it is not an address, it is at a free mailbox provider, or it is on a domain that has nothing to do with this store

The SPF lookup limit is the one that surprises people. Every include: in an SPF record costs a receiver a DNS query, recursively. Ten is the limit, and the eleventh makes the whole evaluation a permanent error, which Gmail treats as a fail. The check follows the includes for real and tells you the number.

A DKIM selector cannot be discovered. A checker either knows the selector name or knows nothing. Where your transport plugin can ask its own provider, the store asks it for you. Where it cannot, put the part before ._domainkey into deliverability.dkim_selector on the Settings screen's Health tab. Two selectors during a rotation is fine; separate them with a comma.

A custom return path passes the SPF check on its own. If you send with em1234.yourdomain.com as the envelope sender and that name points into your provider's zone, your own SPF record does not have to mention them, and the check says so. Set deliverability.return_path where the check cannot work the host out.

Your transport

Check What it reads What a failure means
Plain-text part Whether emails/newsletter-campaign.txt.twig resolves Something has overridden the text half of the campaign template away, so campaigns go out as HTML only, which every spam filter scores against
One-click unsubscribe What your transport plugin says about itself The transport drops custom headers, so List-Unsubscribe never reaches the wire and Gmail shows no unsubscribe button
Sending rate sending.per_minute A warning at either end: zero, which the sender reads as one message a minute, or above six hundred, which is more than a single Grav worker and most provider plans manage
Bounce webhook The providers.<name>.secret for your transport, and the newsletter's event log No webhook is set up, so the store never finds out which addresses bounced; or one is set up and nothing has arrived through it in thirty days

The unsubscribe header matters. The button it puts at the top of a Gmail message is the alternative to the spam button, and a reader who cannot find the first one presses the second. One complaint counts for far more against a store than one unsubscribe.

A missing webhook breaks things slowly. The address that hard bounced is mailed again next month and every month after, and a list with a growing tail of dead addresses looks worse to every receiver each month. See Delivery reports and providers.

Your list

Check What it reads What a failure means
Bounces and complaints Every send in the last thirty days, and the suppression list Complaints at or above 0.3%, which is Gmail's published line; bounces at or above 5%, which is roughly where a mail provider suspends an account; or bounces recorded with an empty suppression list, which means they are being counted and not acted on

Under thirty messages in the window it says nothing, because one bounce out of four is a 25% bounce rate and means nothing. A store that has never sent gets a note saying these fill in after the first campaign.

The bounce rate also raises an alert on the admin's Needs attention strip when it goes above alerts.bounce_rate_percent, five per cent by default. On KahunaCart 1.5.3 or later it also goes to your notification channels as an urgent message, with a "Cleared" one when it falls back.

When a check cannot answer

Two of the nine are questions only the service you send through can answer: what its DNS should look like, and whether a custom header survives its transport. Both are asked of that service's own Grav plugin through the contract the Grav Email plugin owns.

Where no plugin answers, because the Grav Email plugin is older than 5.1.0 or the transport plugin is a release behind, the checks say so rather than guessing. The SPF and DKIM rows say nobody has said what this transport's records should look like, and the unsubscribe row says the transport has not been asked.

Note

A store relaying through its own SMTP server also gets "nobody has asked" on the unsubscribe row. The Email plugin registers no provider for its own smtp, smtps and sendmail engines, so nothing has answered the question either way.

Update the Email plugin and the transport plugin and the rows fill in.

Run the same checks at a terminal

BASH
bin/plugin kahunacart-newsletters deliverability:check
bin/plugin kahunacart-newsletters deliverability:check --cached
bin/plugin kahunacart-newsletters deliverability:check --strict
Flag Effect
--cached Use the ten-minute cache the admin tab reads instead of looking everything up again. Not what you want after changing a DNS record.
--strict Exit 1 on a warning as well as on a failure.

Same nine checks, same three groups, same sentences, translated through Grav's translator.

The exit code is 1 when any check failed and 0 otherwise. Warnings do not fail the run without --strict. A check that could not run, such as on a store with no campaigns behind it, is not a check that failed.

It makes real DNS queries and writes what it finds into the same ten-minute cache the admin tab reads, so the two never disagree. Use it on a support call, or as a check you can fail a build on.

A worked example: kahunacart.com's own records

kahunacart.com sends through SMTP2GO's API transport. Five records do the whole job, and three of them are CNAMEs SMTP2GO hands out during setup.

Name Type Value What it is for
kahunacart.com TXT v=spf1 include:spf.smtp2go.com ~all Says SMTP2GO may send as this domain. One record, one include, two DNS lookups against a limit of ten
em<id>.kahunacart.com CNAME return.smtp2go.net The custom return path, the envelope sender the messages go out with, which is what makes SPF align on this domain rather than on SMTP2GO's
s<id>._domainkey.kahunacart.com CNAME dkim.smtp2go.net The DKIM selector. A CNAME rather than a pasted key, so SMTP2GO can rotate the key without anybody touching DNS again
link.kahunacart.com CNAME track.smtp2go.net Click and open tracking on the store's own hostname, so nothing in a campaign points at a domain the reader does not recognise
_dmarc.kahunacart.com TXT v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100 Tells receivers to quarantine anything claiming to be this domain that fails both SPF and DKIM, and where to send the daily reports

<id> is the SMTP2GO account's own numeric id and is different for every account.

Tip

Start DMARC on p=none. It asks receivers to report and to change nothing, so the reports tell you whether anything legitimate is failing (a help desk, an invoicing service, a form on a different host) before a policy acts on it. Read a month of them, fix whatever is failing, then move to quarantine.

What is not here, and why

A blacklist lookup. DNS blocklists are checked against a sending IP, and on a shared transport that IP is your provider's, so the answer would be about your provider's reputation and not this store's.

A spam-score preview. That means handing a third party the content of a campaign and the store's from address, for a number that changes with the filter you ask. The test send on the campaign screen puts the real message in your own inbox, which tells you more.

A verdict on your sending rate. What a transport accepts is a number on a plan somewhere, different per provider and changing over time, so the check only says when the number is obviously wrong at either end.