justinholtweb/craft-corndog

Every email Craft sends, logged, tracked and testable — system messages, Commerce, form notifications and anything else that goes through Craft’s mailer.

Maintainers

Package info

github.com/justinholtweb/craft-corndog

Homepage

Type:craft-plugin

pkg:composer/justinholtweb/craft-corndog

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-23 14:44 UTC

This package is auto-updated.

Last update: 2026-08-23 15:25:26 UTC


README

Corndog

Corndog

Every email Craft sends, on a stick — logged, readable, and saying which code sent it.

Corndog logs every message that leaves a Craft site — system emails, Commerce, Formie, Freeform, the contact form, a craft.app.mailer call in a template, and any plugin that has not been written yet — and tells you what was in it, where it came from, whether it left, and what the mail server said if it did not.

It is a Craft answer to WordPress's Check & Log Email, with the part that plugin charges for — which code sent this email — included, because that is the column you actually need at 4pm on a Friday.

  • Free, single edition, everything switched on.
  • No runtime dependencies, no build step, no outbound requests of its own.
  • Craft 5.3+, PHP 8.2+.

What it does

Logs everything. One row per send attempt: recipients, sender, reply-to, cc, bcc, subject, both bodies, headers, attachment names and sizes, the transport that carried it, the Message-ID the server handed back, how long the send took, the site, the logged-in user, the IP, and the URL or console command that triggered it.

Says where it came from. Corndog reads the call stack at the moment of the send, so the log says Craft Commerce, Formie, System message: forgot_password, or Template: newsletter/_weekly.twig — with the class, method and queue job behind it. When two order confirmations go out, this is the column that tells you which plugin sent the second one.

Catches failures with their reason. A transport that throws — bad credentials, refused connection, 550 relay denied — is caught between the mailer and the transport, so the log carries the actual message rather than “unsuccessful”. Craft, left alone, swallows that exception and returns false.

Emails you when sending breaks. An expired API key is silent: the site carries on, and nobody finds out until a customer says they never got their receipt. Set a threshold and a window, and Corndog tells you.

Resends, with edits. Change the recipient, the subject or the body and send it again. The resend is logged too, and points back at what it was a resend of.

Tests properly. Craft's own test-email button answers “did the transport accept a message”. Corndog's test screen sends a real message with headers you choose, then hands you the log entry: transport, headers, Message-ID, error text and all.

Tracks opens and clicks — if you ask it to. Off by default, deliberately.

Stops mail leaving. One switch: log every message, deliver none. This is what a staging site restored from a production database needs, before it emails a thousand real customers.

Redirects and blind-copies. Send everything to one address, or blind-copy every outgoing message to an archive.

Trims itself. Retention in days and a hard row cap, applied during Craft's garbage collection — not on the request of whoever happened to trigger the five-thousandth email.

Exports. CSV of whatever the filters are showing.

Installing

composer require justinholtweb/craft-corndog
php craft plugin/install corndog

Then Corndog → Email log in the control panel. There is nothing to configure to get started; the defaults log everything, keep 60 days, and track nothing.

Settings

Settings live under Settings → Plugins → Corndog, and every one of them can go in config/corndog.php instead:

<?php

return [
    'enabled' => true,

    // What to keep
    'storeHtmlBody' => true,
    'storeTextBody' => true,
    'storeHeaders' => true,
    'storeAttachmentDetails' => true,
    'maxBodySize' => 512,          // KB
    'storeIpAddresses' => true,
    'anonymiseIpAddresses' => true,

    // How long
    'retentionDays' => 60,         // 0 keeps everything
    'maxRows' => 5000,             // 0 for no cap

    // Tracking (off by default)
    'trackOpens' => false,
    'trackClicks' => false,
    'ignoredLinkPatterns' => ['*unsubscribe*', '*/actions/*'],

    // Delivery overrides
    'stopSending' => false,
    'redirectAllTo' => '',
    'bccAll' => [],

    // Failure alerts
    'alertOnFailure' => false,
    'alertTo' => '',
    'alertThreshold' => 3,
    'alertWindow' => 60,           // minutes
    'alertCooldown' => 60,         // minutes
];

Tracking, and being straight about it

Open tracking loads a 1×1 image from your site. Click tracking rewrites links so they pass through your site first. Both are off until you switch them on, because knowing whether a named person opened an email is personal data in most of the world, and that is your decision rather than a plugin's.

If you do switch them on:

  • Say so in your privacy policy.
  • Leave unsubscribe links alone. They are excluded by default, and routing that particular click through a tracker is tracking the one click somebody made specifically to stop being tracked.
  • Add data-corndog-ignore to any link you want passed through untouched.
  • Remember that mail clients block images. An unopened email and a cautious reader look identical from here, so an open rate is a floor, not a measurement.

Click URLs are signed with Craft's security key. An unsigned or edited target is never followed — otherwise the endpoint would be an open redirect on your own domain, which is exactly what a phishing campaign goes looking for.

Permissions

  • View the email log — the log and message screens
    • Read message bodies — what the email actually said
    • Resend messages and send tests
    • Delete log entries

Console

php craft corndog/email/stats --days=30   # what has gone out, and what has not
php craft corndog/email/test you@example.com
php craft corndog/email/prune             # apply the retention settings now
php craft corndog/email/prune --dry-run
php craft corndog/email/clear

Twig

{% for message in craft.corndog.recent(10) %}
    {{ message.subject }} → {{ message.toLine }} ({{ message.statusLabel }})
{% endfor %}

{{ craft.corndog.total({ status: 'failed' }) }}
{{ craft.corndog.failures(60) }}          {# failures in the last hour #}
{{ craft.corndog.summary(30).sent }}
{{ craft.corndog.daily(30)|json_encode|raw }}

What it cannot see

Corndog hooks Craft's mailer. Anything that does not go through it is invisible — a plugin calling PHP's mail() directly, a JavaScript form posting to a third-party service, a transactional email your ESP sends on its own schedule. That is not a gap you can close from inside Craft: Craft cannot see those either.

Rows are written inside whatever database transaction is running at the time, so a send inside a transaction that later rolls back takes its log row with it.

Storage

Bodies are the biggest thing in the table by an order of magnitude. If the log is bigger than you want it to be, in order: lower maxRows, lower maxBodySize, then turn storeHtmlBody off. The list screen never selects a body, so a large log stays a fast screen either way.

Documentation

Full documentation is at justinholt.com/plugins/craft-corndog/docs — installation, every setting, the log and the message screen, tracking, troubleshooting and an FAQ.

Licence

The Craft License. See LICENSE.md. Corndog is free: no editions, no licence key, and no licensing code in the plugin.