11 KiB
The surface Nolimit Send exposes
Everything another plugin, a theme or a site owner can bind to, store against, or break by changing. It describes what the installed plugin actually does, not what it might one day do: a table written from intent lists what a plugin means to provide rather than what it provides.
The contract covers the files inside the installed plugin. Nothing else is part of it.
Hooks this plugin provides
None. This plugin fires no do_action() and applies no apply_filters()
of its own, so there is nothing here for anyone to hook.
That is a deliberate state rather than an oversight, and it is worth one line so nobody adds one casually: every extension point is a promise that outlives the reason it was added, and nothing has yet asked to extend this. In particular there is no filter that lets another plugin change the SMTP settings — that is exactly the door a credential leaks through. The moment something does ask, the filter goes in this table in the same commit.
Hooks this plugin registers on
🔴 Deliberately not headed "consumes". add_filter() is how you answer a
filter — you supply the value somebody else asked for — and reading it as
consumption gets the direction of the promise backwards. For an action the
word is listens; for a filter it is answers. The Kind column below says
which.
Priority and accepted-argument count are given where they are not WordPress'
defaults of 10 and 1, because those two are what break silently — see the
update_plugins_ row.
| Hook | Kind | Registered in | Why |
|---|---|---|---|
phpmailer_init |
action | includes/class-mailer.php |
The whole job: points PHPMailer at the chosen server. Does nothing unless enabled, with a host, and no other mail plugin present. |
wp_mail_failed |
action | includes/class-mailer.php |
Keeps the reason a message failed — time and message only — when this plugin was the one sending. |
wp_mail_from |
filter, priority 99 | includes/class-mailer.php |
The sender address. 99 so WooCommerce's own callback at 10 has already had its say. |
wp_mail_from_name |
filter, priority 99 | includes/class-mailer.php |
The sender name, on the same rule. |
plugins_loaded |
action | includes/class-plugin.php |
Everything else is registered from here. |
admin_menu |
action | includes/class-admin.php |
Registers the screen under the shared Nolimit menu. |
admin_init |
action | includes/class-admin.php |
Handles the save, test and clear form posts, after the nonce and capability checks. |
admin_init |
action | includes/class-updater.php |
Drops the cached feed when the user presses "Check again". |
admin_enqueue_scripts |
action | includes/class-admin.php |
The one stylesheet, on this plugin's screen only. |
admin_post_nlsnd_auto_update |
action | includes/class-auto-update.php |
The auto-update toggle's form target. Name is this plugin's; the hook is WordPress'. |
plugin_action_links_<basename> |
filter | includes/class-admin.php |
The "Settings" link on the Plugins screen. Hook name is built from NLSND_PLUGIN_BASENAME. |
plugins_api |
filter, 3 args | includes/class-updater.php |
The "View details" panel. Guarded on $args->slug. |
update_plugins_<hostname> |
filter, 3 args | includes/class-updater.php |
Offers this plugin's own updates. |
upgrader_process_complete |
action, 0 args | includes/class-updater.php |
Forgets the cached feed after any update runs. |
send_test() also adds and removes a wp_mail_failed listener around one
wp_mail() call, so the test button can say why a message did not go out. It
is removed again on the way out.
Nothing hooks wp_mail itself. No filter here can alter a message's
recipient, subject, body, headers or attachments, and the plugin's own checks
assert that wp_mail has no callback from it. That is the property that lets
the plugin be site-wide: it configures the transport and leaves the message
alone.
🔴 update_plugins_<hostname> is shared, and the argument count is load-bearing
The hook name is built from the host, not the repository, so every plugin
updating from gitea.nolimit.dk lands on the same filter and WordPress runs
every callback on it for every one of them. Answering unconditionally offers
this plugin's release to a sibling — which has happened on a live shop, and
installs a stranger's zip over a plugin with auto-updates on.
The defence is two halves and neither works alone: add_filter( …, 10, 3 )
and a guard returning $update untouched when $plugin_file is not this
plugin's basename. Leave the count at 2 and the third argument never
arrives, so the guard compares against an empty string and silently does
nothing — the same bug it was written to prevent, wearing a fix's clothes.
A test on a site with one plugin from that host cannot see this, because the collision cannot arise there. The way to check it is to fire the real filter in both directions: the neighbour is left alone, and this plugin still answers for itself. The second half is not redundant. A guard that bails on everything also leaves the neighbour alone.
The family's shared hooks
These are the Nolimit-family surface, not this plugin's.
| Hook | This plugin's relationship | Where |
|---|---|---|
wpmgr_dashboard_cards |
answers — supplies this plugin's card | includes/class-admin.php |
wpmgr_dashboard_cards |
applied by the shared file this plugin ships | includes/shared-menu.php |
wpmgr_menu_title |
applied by the shared file this plugin ships | includes/shared-menu.php |
wpmgr_menu_glyph |
applied by the shared file this plugin ships | includes/shared-menu.php |
admin_head |
listened to by the shared file this plugin ships | includes/shared-menu.php |
🔴 Shipping a file is not owning its hooks
includes/shared-menu.php is byte-identical across the Nolimit plugins.
Whichever copy loads first declares the functions; every later copy sees
them already defined and does nothing. So the three wpmgr_*
filters above are applied by a copy of that file, which on any real site is
probably another plugin's. Anyone depending on those filters should depend on
the family, not on this plugin being installed.
The card this plugin answers with carries title, status, headline,
lines, url, action and colour. colour is NLSND_COLOUR, which the
shared file paints as a card header with white text.
The provider table — shared with Nolimit Forms
NLSND_Mailer::providers() is hand-copied from NLFRM_Mailer::providers()
in Nolimit Forms, which has an SMTP mailer of its own. Nothing shares the code
across the two plugins, so a difference between the copies is a defect that
neither plugin can see. The values that must agree, by id:
| Id | Host | Port | Encryption |
|---|---|---|---|
brevo |
smtp-relay.brevo.com |
587 | tls |
smtp2go |
mail.smtp2go.com |
2525 | tls |
mailgun |
smtp.mailgun.org |
587 | tls |
sendgrid |
smtp.sendgrid.net |
587 | tls |
postmark |
smtp.postmarkapp.com |
587 | tls |
ses |
(none — region-specific, typed in) | 587 | tls |
google |
smtp.gmail.com |
587 | tls |
microsoft |
smtp.office365.com |
587 | tls |
custom |
(none — typed in) | 587 | tls |
Cautions such as the Microsoft one are kept out of the table on purpose
(NLSND_Mailer::warning()), so one can change without touching the contract.
Both plugins hook phpmailer_init. With both switched on, the later
callback wins. Nolimit Forms is told this plugin is present by the constant
NLSND_VERSION, which is defined at include time for exactly that reason.
Nolimit Send reports Forms' SMTP on its own screen as a notice rather than
standing down for it.
Stored data
Every row here is removed by uninstall.php on delete — not on deactivate,
because switching a plugin off for ten seconds to test something must not
destroy the shop's mail settings.
| Key | Kind | Holds |
|---|---|---|
nlsnd_settings |
option, not autoloaded | Every setting, including the SMTP password as typed. |
nlsnd_last_failure |
option | The last delivery failure: a time and a message capped at 400 characters. |
nlsnd_release_feed |
transient, 15 min | The cached update feed. |
nlsnd_auto_update_seeded |
option | A marker, so the auto-update default is offered once and never re-applied. |
The password is stored in clear because WordPress has nowhere to keep a secret; anyone who can read the options table can read it. It is never rendered back into a form. Mail content — recipient, subject, body — is never stored, and there is no send log.
Capabilities
| Capability | Gates |
|---|---|
manage_options |
Seeing the screen, saving settings, sending a test, clearing the last failure. |
update_plugins |
The auto-update toggle only. WordPress' own capability for the same decision. |
Constants
NLSND_VERSION, NLSND_PLUGIN_FILE, NLSND_PLUGIN_DIR, NLSND_PLUGIN_URL,
NLSND_PLUGIN_BASENAME, NLSND_COLOUR.
NLSND_VERSION has a reader outside this repository: Nolimit Forms asks whether
it is defined. It must stay defined at include time, not from a hook.
NLSND_COLOUR (#556b0f) is the other one with a reader outside this
plugin: the shared menu paints it as a dashboard card background with
hardcoded white text, which it measures at 6.01:1 against white, over the 4.5:1
the 14px label needs. It exists in three places that must stay the same string —
the define(), the stylesheet's --nlsnd-accent, and assets/icon.svg.
What is deliberately not here
Stated because an absent surface is invisible, and the next person to want one should find the reason rather than the silence.
- No WP-CLI commands, no REST routes, no AJAX endpoints. The screen is a
form post handled on
admin_init. - No JavaScript. One stylesheet, no script is enqueued.
- No cron, no scheduled events. Nothing runs on a timer; the update feed is fetched lazily behind a transient.
- No queue, throttle or retry. A failed message is recorded and not re-sent.
- No send log, and no bounce or complaint handling. SMTP cannot report them.
- No OAuth. Google needs an App Password; Microsoft's password sign-in is being retired and the screen says so.
- No hook on
wp_mail, and no filter over the SMTP credentials. - No user meta, post meta or term meta.
- No telemetry of any kind. The only network traffic is the mail itself, to the server the owner named, and the update check against the public release repository, which sends no credential.