For developers · v2.7 · GPLv2

Two filters, one row per email, no vendor.

Everything below is verifiable in the source. This page exists so the technical detail does not have to be inferred from a feature list, and so you can decide in five minutes whether this belongs in your stack.

WordPress 6.0 to 7.0PHP 7.4+PHPMailer 7.0.2MultisiteRead-only filesystem

Integration surface

Where it attaches
to WordPress.

The sending path and the logging path are separate. Logging is a passive observer ofwp_mail(), so it keeps working whatever transport you end up using, including none of ours.

HookPriorityWhat happens there
wp_mail_from
wp_mail_from_name
PHP_INT_MAXSender identity, applied last so a theme or another plugin cannot silently win the argument.
phpmailer_initPHP_INT_MAXTransport configuration: host, port, encryption, auth, Return-Path.
pre_wp_mail10Short-circuits the whole PHPMailer path when the SES API mailer is selected, and builds raw MIME instead.
wp_mailPHP_INT_MAXCaptures recipient, subject, headers and attachment count in memory. Nothing is written yet.
wp_mail_succeeded
wp_mail_failed
default, then 20The logger writes the row, then the monitor updates its counters. One write, after the outcome is known.
custom_smtp_purge_logsdaily cronA single prepared DELETE for everything older than your retention window.
custom_smtp_alert_check15 min cronVolume thresholds only, and only when alerts are switched on. Failure streaks are counted inline instead.
after_password_reset
notification filters
plugins_loadedOptional switches for WordPress's own admin notification emails: password change, new user, auto-update results, comments. Nothing is off by default.
custom_smtp_skip_logfilterYours. Return true to exclude the current send from the log and from the alert counters.

What it costs

One INSERT.
That is the footprint.

Worth stating precisely, because “email logging” is the kind of feature that quietly turns into a performance incident two years later.

  • One row written per email, on the success or failure hook. The capture step holds the data in memory, so there is no insert-then-update dance and no orphan row when a send dies halfway.
  • No query at all on a page that sends nothing, which is nearly all of them. The plugin registers filters and gets out of the way.
  • One table, three indexes: date_sent, status, and a 100 character prefix index on recipient. Those are the three columns the log screen actually filters on.
  • Bodies are not stored by default. When you do enable content logging, each body is capped at 500 KB so a rogue newsletter cannot bloat the table.
  • Purging is one DELETE a day, on a WP-Cron hook, using the retention you set. Set it to 7 on a busy store and the table stays small permanently.
  • No outbound HTTP request other than to your mail provider, and to your own webhook if you configured one. No licence check, no telemetry, no update ping outside the WordPress.org mechanism every plugin uses.

Where state lives

Settings in wp_options as a single array, logs in one custom table, alert counters in options, CSV exports streamed to the browser rather than written to disk. Nothing is written to the plugin directory at runtime, so a read-only filesystem andDISALLOW_FILE_MODS are both fine.

Amazon SES · API · IAM role

Send from AWS with no credentials at all.

On ECS Fargate, EC2 or App Runner, Custom SMTP sends through the SESv2 API using the task or instance role. Nothing is pasted into wp-admin, nothing is written to the database, nothing to rotate.

  • Zero stored secrets. Authentication comes from the AWS default credential chain, so the container gets its permissions from its role.
  • PHPMailer is bypassed entirely. The mailer short-circuits pre_wp_mail and submits raw MIME, so HTML, attachments, CC, BCC, Reply-To and custom headers all survive. BCC stays hidden.
  • Configuration Sets are supported, for bounce and complaint tracking in CloudWatch.
  • No hard dependency. It uses the AWS SDK when the image already ships one, and falls back to a small built-in signed client when it does not.

Container environment

# mailer selected network wide, no per-site setup
CUSTOM_SMTP_MAILER_TYPE=ses_api
SES_REGION=eu-west-3
SES_FROM_EMAIL=no-reply@example.com
SES_CONFIG_SET=transactional

# any setting can be forced the same way
CUSTOM_SMTP_LOG_RETENTION=90
CUSTOM_SMTP_STORE_CONTENT=0

Anything supplied by the environment appears read-only in wp-admin with a badge naming the variable it came from, so an administrator can see what the platform controls and stops filing tickets about fields that will not save. The same values can be defined as PHP constants in wp-config.phpif you would rather keep configuration in code than in the environment.

Multisite

Network settings,
per-site overrides.

Configure the network once and every site inherits it. A site can override what you let it override, and cannot touch what the environment has already pinned.

Precedence runs in one direction and one direction only: environment variables and PHP constants win, then network settings, then the individual site’s own options. That ordering is what makes a fleet predictable, because a client administrator clicking around their own dashboard cannot undo a platform decision.

The log stays per site, which is almost always what you want: the person looking at it is looking for one message, on one site, sent to one person.

Security posture

Stated, not implied.

A plugin that holds SMTP credentials and a copy of your outbound mail should be explicit about how it handles both.

  • Passwords are encrypted with AES-256-CBC, keyed on your WordPress salts. No key ships inside the plugin, which also means credentials do not survive a restore into a site with different salts, by design.
  • Every AJAX endpoint checks a nonce and a capability before doing anything. Test connection, test email, export, delete, preview and resend are all gated.
  • Every query is prepared. User input reaching the log search is passed through esc_like() and bound, never concatenated.
  • Exports are streamed, so nothing lands in the filesystem and nothing is left behind in an uploads directory for someone to find later.
  • Alert mail cannot loop. The notifier marks its own messages so they are neither logged nor counted towards the thresholds that triggered them.
  • The plugin does not evaluate remote code, load remote assets, or fetch anything from a vendor endpoint at runtime.

Reporting something

Security reports go through the plugin’s page on WordPress.org rather than a public forum thread. The repository listing carries the current contact and the full changelog, version by version.

Extending it

One filter you
will actually use.

High-volume transactional traffic you do not want in the log, or a message class you are not allowed to retain, both come down to the same hook.

Excluding a send from the log and the counters

// Keep queue traffic out of the log entirely.
add_filter( 'custom_smtp_skip_log', function ( $skip ) {
    if ( wp_doing_cron() && defined( 'MY_BULK_RUN' ) ) {
        return true;
    }
    return $skip;
} );

Abilities API

The plugin registers two abilities for the WordPress Abilities API, so an agent or an orchestration tool can check the mail path without screen-scraping wp-admin:custom-smtp/connection-health andcustom-smtp/send-test-email. Both respect the same capability checks as the admin screens.

Everything is translated

Text domain custom-smtp, French shipped in full, including the translated SMTP error messages, which is the part that usually stays in English and matters most to the person reading it.

Compatibility

What it runs on.

Tested against current WordPress, and written against the PHP version your oldest client is still on.

RequirementSupportedNotes
WordPress6.0 to 7.0Uses the WP 6.9 Envelope-From and Return-Path handling when it is available.
PHP7.4 and upTyped properties and return types, no PHP 8 only syntax.
PHPMailer7.0.2Configured on phpmailer_init, never replaced or vendored.
MultisiteyesNetwork settings with per-site overrides.
Read-only filesystemyesNothing written outside the database. Streamed exports.
Object cacheyesSettings are read once per request and held in memory.
OpenSSLrequiredOnly for encrypting stored passwords at rest.

Read the source,
then decide.

It is GPLv2 on the WordPress.org repository, so the code is one download away and there is no account between you and it.

v2.7GPLv2No dependencyNo telemetry