1 Commits
Author SHA1 Message Date
Nolimit release 858868f338 Update the public pages to 0.1.0 2026-10-07 10:01:36 +02:00
8 changed files with 412 additions and 3 deletions
+37 -3
View File
@@ -1,5 +1,39 @@
# Nolimit Send: releases # Nolimit Send
This repository holds the release zips for the Nolimit Send WordPress plugin, and nothing else. WordPress reads it to offer updates. ![Nolimit Send — the screen where you pick how the site sends mail](assets/hero.png)
Download the latest zip from Releases. Chooses how WordPress and WooCommerce send email. Pick SMTP2GO, Brevo, Mailgun,
SendGrid, Postmark, Amazon SES, Google or Microsoft — or type in any SMTP
server — and every message the site sends goes through it. One screen, no
account, no telemetry, no paid tier.
## Install
1. Download `nolimit-send.zip` from the [latest release](https://gitea.nolimit.dk/kw/nolimit-send-dist/releases/latest).
2. **Plugins → Add New → Upload Plugin**, then activate.
3. Open **Send** in the Nolimit menu, pick a provider, and press
*Send a test message* — [the setup guide](docs/SETUP.md) says what each
provider wants in the password field.
Nothing is sent through it until you switch it on. Later versions arrive in
**Dashboard → Updates** like any other plugin.
## Documentation
- [Overview](docs/README.md) — what it does, and what it deliberately does not
- [Setting it up, and knowing it works](docs/SETUP.md) — provider by provider,
how to check a message really went through your provider, and what to do
when mail stops
- [Hooks and integration](docs/CONTRACT.md) — for developers: what the plugin
hooks into, what it stores, and what it leaves alone
## More of it
| | |
|---|---|
| ![Microsoft 365 preset](assets/screenshots/microsoft.png) | **Honest about Microsoft 365.** Password sign-in for SMTP is being retired and this plugin does not do OAuth, so the Microsoft preset says so on the screen instead of working until it quietly stops. |
| ![A failed test message](assets/screenshots/test-failure.png) | **The server's own answer.** A failed test shows what the mail server said, and the last real failure is kept with its time, so "why did that email not arrive" has an answer. |
| ![What the test can and cannot tell you](assets/screenshots/limits.png) | **No pretending.** SMTP hands a message over; it cannot report bounces or spam complaints. The screen says what a test proves and what only your provider's dashboard can tell you. |
Requires WordPress 5.9+ and PHP 7.4+. WooCommerce is not required, but it is
what the plugin is tested against.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

+198
View File
@@ -0,0 +1,198 @@
# The surface Nolimit Send exposes
<!-- nlsnd:version 0.1.0 -->
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.
<!-- nlsnd:hooks:start -->
## 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` |
<!-- nlsnd:hooks:end -->
### 🔴 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.
+68
View File
@@ -0,0 +1,68 @@
# Nolimit Send
<!-- nlsnd:version 0.1.0 -->
Chooses how WordPress and WooCommerce send email. Pick a provider — SMTP2GO,
Brevo, Mailgun, SendGrid, Postmark, Amazon SES, Google, Microsoft — or type in
any SMTP server, and every message the site sends goes through it.
## The problem it was written for
By default WordPress hands mail to the web host's own `mail()`. That is the
mail that lands in spam, arrives from `wordpress@` a domain that has no SPF
record for the host, or is silently dropped — and a shop only finds out when a
customer writes to say the order confirmation never came.
The fix is to send through a real mail service over SMTP. The plugins that do
it are usually large, and several of them phone home or sell the upgrade on
the same screen where the password goes. This one is a single screen.
## What it does, and what it refuses to do
* **Routes everything.** It configures PHPMailer, the library behind
`wp_mail()`, so WordPress itself, WooCommerce orders and receipts, password
resets and every other plugin's mail go through the chosen server. It is
site-wide, not WooCommerce-only.
* **Presets** that fill in the host, port and encryption, with a note per
provider on what the password field really wants (Brevo wants an SMTP key,
Google an App Password).
* **A test message**, sent from the screen, that shows the server's own
answer when it fails.
* **The last failure**, kept and shown with a time, so "why did that email not
arrive" has an answer on the screen rather than in a log nobody opens.
* **A sender address and name**, replacing WordPress' default `wordpress@…` —
or, if forced, every sender.
What it refuses to do:
* **It never touches a message's own headers.** Reply-To, Cc, List-Unsubscribe
and anything a caller set are passed through exactly as they came.
* **It does not queue, throttle or retry.** A failed message is recorded, not
re-sent.
* **It cannot report bounces or complaints.** SMTP hands a message over; whether
it was delivered comes back, if at all, to the provider's dashboard. The
screen says so rather than implying a delivery report.
* **It does not do OAuth.** Google works with an App Password. Microsoft 365 is
retiring username-and-password SMTP, and the screen says that plainly on the
Microsoft preset — [the setup guide](SETUP.md#microsoft-365) has the dates.
* **It stands down** when another SMTP plugin (WP Mail SMTP, FluentSMTP, Post
SMTP, Easy WP SMTP, Brevo for WordPress) is active, and names it on the screen.
## Documentation
| File | For |
| --- | --- |
| [SETUP.md](SETUP.md) | Installing it, choosing a provider, checking it works, and what to do when mail stops. |
| [CONTRACT.md](CONTRACT.md) | For developers: every hook, option, capability and constant — and what is deliberately absent. |
## Good to know
* **No telemetry**, no account, no upsell. Nothing is sent anywhere except the
mail itself, to the server you named, and the update check against the
public release page, which carries no credential.
* **Updates** come from that release page rather than wordpress.org, and appear
in **Dashboard → Updates** like any other plugin's.
* The SMTP password is stored as typed in one option row, because WordPress has
nowhere better to keep it. It is never printed back into the form, and it is
removed when the plugin is deleted.
* The strings are English only.
+109
View File
@@ -0,0 +1,109 @@
# Setting up Nolimit Send
<!-- nlsnd:version 0.1.0 -->
## Installing it on a shop
| Needs | Version |
| --- | --- |
| WordPress | 5.9 or newer |
| PHP | 7.4 or newer |
| WooCommerce | Not required. It is tested with WooCommerce (6.0 or newer) because that is the mail most shops care about |
Download `nolimit-send.zip` from the
[latest release](https://gitea.nolimit.dk/kw/nolimit-send-dist/releases/latest),
install it through **Plugins → Add New → Upload Plugin**, and activate.
After the first install, updates arrive through the normal WordPress updates
screen — the plugin carries an `Update URI`, so it checks its own release feed
and is never asked about on wordpress.org.
**Nothing is sent through it until it is configured.** After activation the
plugin is off: WordPress keeps sending the way it did. That is deliberate — a
mailer that switched itself on with no server behind it would take every email
the shop sends down with it.
## Choosing how the site sends
The screen is **Send**, under the shared Nolimit menu in the admin sidebar,
and needs the `manage_options` capability.
1. Tick **Send through this server** and pick a provider. The host, port and
encryption are filled in from the preset; choosing a different provider
replaces all three, so there is never a mixed pair such as one provider's
name over another's host.
2. Enter the username and password the way the note under the field says. What
the field wants differs, and is the usual cause of a rejected login:
| Provider | What goes in the password field |
| --- | --- |
| Brevo | An **SMTP key** from SMTP & API, not the account password and not an API key |
| SMTP2GO | That SMTP user's password. Port 2525 is the default: it is open from more networks than 587 |
| Mailgun | That SMTP user's password from the Mailgun dashboard |
| SendGrid | An API key with Mail Send permission; the username is the literal word `apikey` |
| Postmark | The server API token, in **both** the username and the password field |
| Amazon SES | The SES **SMTP** password, and the region's host typed in |
| Google | An **App Password**, which needs two-step verification on that account |
| Microsoft | The mailbox password, with SMTP AUTH enabled for that mailbox — see below |
| Other | Whatever your server expects |
3. Optionally set a sender address and name. Unless **Always use this sender**
is ticked, they replace only WordPress' own default `wordpress@…` /
`WordPress` and leave any sender a plugin chose alone.
4. **Save**, then **Send a test message**. A failure shows the server's own
answer. A real failure is kept with its time; the status reads "a message
failed recently" for 24 hours, and a button clears it sooner. A later success
does not clear it early.
### Microsoft 365
Username-and-password SMTP is being retired: switched off by default for
existing organisations from the end of December 2026, unavailable to new ones,
with a final date not yet announced. **This plugin does not do OAuth**, which is
what replaces it, so the Microsoft preset works until an administrator loses the
ability to enable it. For anything that has to keep working, use a sending
provider. The screen says the same thing on the preset itself.
### Other mail plugins
If WP Mail SMTP, FluentSMTP, Post SMTP, Easy WP SMTP or Brevo for WordPress is
active, this plugin does **not** send: two plugins that both configure PHPMailer
produce whatever the later one says. The screen names the plugin. Nolimit Forms
has an SMTP mailer of its own; that is only a notice, and the owner decides
which to switch off.
### Auto-updates
The plugin offers to switch its own auto-updates on, once, through a notice on
its screen. Taking or dismissing the offer writes a marker
(`nlsnd_auto_update_seeded`) so it is never asked again — the setting is
WordPress' own, in WordPress' own place, and the toggle is gated on
`update_plugins`.
## Knowing it works
* **Send a test message** from the screen. It goes through the server you chose
and, when it fails, shows that server's own answer — a refused login, a host
that cannot be reached, a sender the provider has not verified.
* **Read the status line** at the top of the screen. It reads *Sending through
<provider>* when everything is in place, *Not sending yet* when it is switched
off or has no host, *Mail is handled by <plugin>* when another mail plugin is
in the way, and *A message failed recently* after a real failure.
* **Look at one real email.** Place a test order or use the password-reset
form, and open the message's full headers in the receiving mailbox. The
`Received:` lines should name your provider, not your web host.
A test message reaching the provider proves the login and the route. It does not
prove the message reached an inbox: that answer lives in the provider's own
dashboard, and so do bounces and spam complaints.
## If mail stops
1. The screen's **last failure** card shows the most recent error with its time.
It is only recorded when this plugin was the one sending.
2. A password or key that was rotated at the provider is the commonest cause.
Enter the new one and save; the field is never filled in for you, and
leaving it empty keeps the stored one.
3. Untick **Send through this server** to hand sending straight back to
WordPress while you sort it out. Nothing else is changed.
4. Deleting the plugin from the Plugins screen removes every setting it stored,
including the password.