Free · no editions, no licensing code
Which code sent that email?
A customer got two order confirmations, a password reset never arrived, and somewhere a plugin is emailing people nightly. Craft's mailer is handed a message, not a reason — so none of those questions have an answer once the send is over. Corndog logs every message that goes through that mailer, keeps what was in it, and reads the call stack at the moment of the send so the log can say which plugin, template, queue job or system message asked for it.
Two commands, then nothing
There is no configuration step and no template change. From the moment it installs, every message that goes through Craft's mailer is logged — and Twig gets the log too, if you would rather build your own screen.
composer require justinholtweb/craft-corndog
php craft plugin/install corndog
{# Nothing needs to go in a template. What Twig gets, if you want it: #}
{% for message in craft.corndog.recent(10) %}
{{ message.subject }} → {{ message.toLine }} ({{ message.statusLabel }})
{% endfor %}
{{ craft.corndog.failures(60) }} {# failures in the last hour #}
{{ craft.corndog.summary(30).sent }} {# sent in the last 30 days #}
# And over SSH, when the question is "is mail still going out?"
php craft corndog/email/stats --days=30
php craft corndog/email/test you@example.com
Features
Everything Check & Log Email keeps for its premium tier, and the part Craft itself cannot tell you.
One hook, every email
Corndog attaches to Craft's mailer rather than to any one plugin, so system messages, Commerce, Formie, Freeform, the contact form, a craft.app.mailer call in a template and anything installed next month all land in the same log.
- Not a list of supported plugins — the place they all go through
- Queue jobs and console sends are logged with their context
It names what sent it
The stack is read at the moment of the send, because that is the only place the reason still exists. Every row carries the plugin, the system message key, the template path, the queue job class, and the file and line.
- “Craft Commerce” and “Template: newsletter/_weekly.twig”
- The premium feature in the plugin this one answers to
Failures with the reason attached
Yii fires its after-send event only when the send returns, and Craft catches the transport exception and answers false — so the mail server's actual words are lost. Corndog stands between the mailer and the transport and keeps them.
- 550 5.7.1 relay access denied, on the message itself
- Optional alert when several sends fail inside a window
What was actually sent
The HTML body as the recipient saw it, the plain-text part, the headers, the transport, the Message-ID and the attachment names — with the body rendered in a sandboxed frame, because an email is a document written by somebody else.
- Resend it with edits, or send a test with your own headers
- Reading bodies is its own permission, because bodies hold reset links
Tracking you have to ask for
Open and click tracking are both off until you switch them on: knowing whether a named person opened an email is personal data in most of the world. Click targets are signed, unsubscribe links are never rewritten, and IPs are blurred by default.
- data-corndog-ignore leaves any single link alone
- An unsigned target is refused rather than redirected
Staging cannot mail your customers
One switch logs every message in full and delivers none of them — for the staging site restored from a production database, where the addresses in it are real. Or send everything to one address instead, and blind-copy an archive.
- Retention in days and a hard row cap, trimmed by garbage collection
- CSV export, a dashboard widget, and prune from the console
Frequently Asked Questions
The questions worth answering before you install it.
Yes. One edition, everything switched on, no licence key and no licensing code in the plugin. The “which code sent this email” feature that the WordPress plugin this one answers to sells as a premium add-on is the centrepiece here.
Yes — and anything else. Corndog hooks Craft's mailer at the level every one of those goes through, so system messages, Commerce order emails, Formie and Freeform notifications, the Contact Form plugin, a craft.app.mailer call in a template, a queue job and a console command all land in the same log, each attributed to whatever sent it.
Anything that does not go through Craft's mailer: a plugin calling PHP's mail() directly, a JavaScript form posting to a third-party service, or a transactional email your delivery provider sends on its own schedule. Craft cannot see those either — it is not a limitation Corndog could remove.
Only if you ask it to. Out of the box it reads the message and stores it. Turning on click tracking rewrites links; turning on open tracking adds an image; the delivery overrides change recipients or hold the send. Every one of those is off by default, and the log records the message as it actually went out.
Nothing in the capture path is allowed to throw — every entry point wraps its work and warns into Craft's log instead, because a logging plugin that can stop a password reset going out is worse than no logging plugin. The cost is two small queries per email.
That depends on where you and your recipients are, and it is your decision rather than a plugin's — which is why both tracking switches ship off. In the EU and UK, tracking whether an identified person opened an email is generally processing personal data and usually needs consent. If you switch it on: say so in your privacy policy, leave unsubscribe links alone (they are excluded by default), and exclude any individual link with data-corndog-ignore.
Whatever was in the email, plus the IP of whoever triggered the send — blurred by default to the network rather than the address, and switchable off entirely. Bodies, headers and attachment details can each be turned off, and retention is capped by days and by row count so the log does not become an archive by accident.
Click rates are close to accurate: a click is a real request from a real browser. Open rates are a floor and nothing more — mail clients block images, proxy them, or prefetch them, and Apple's Mail Privacy Protection loads every image the moment a message arrives whether anybody read it or not. Corndog reports what happened rather than modelling what it thinks happened.
Yes, and that is one of the reasons it exists. Stop sending logs every message in full and delivers none of them; send everything to replaces every recipient with one address. Put either in config/corndog.php under a staging key and it cannot be left on in production by accident.
A row without a body is roughly a kilobyte; with an HTML body it is as big as the email. The defaults keep 60 days and cap the table at 5,000 rows, which is a few tens of megabytes for a typical site. Both are settings, and the list screen never loads a body — so a large log stays a fast screen.
Craft's test email answers “did the transport accept a message addressed to me”. Corndog answers that too, and then keeps the message: the transport it used, the headers it carried, the Message-ID the server returned, and the exact error if it failed — the things you need when the question is not “does mail work” but “what happened to this one email on Tuesday”.
Both tables are dropped and the log goes with them. Nothing else changes: Corndog never alters how your site composes mail, so removing it leaves the site sending exactly what it sent before.
Install it before you need it
Free, for Craft CMS 5.3 and later. The email you will want to look at is the one that has already been sent — so the useful time to install a mail log is the day before something goes wrong with it.