Separate the form draft from the tested configuration
- Browser draft
- The valid values currently visible in the admin form. They are not durable until Save changes succeeds.
- Persisted settings
- The instance configuration returned by
get_email_configuration()inside the test endpoint. - Configuration gate
- Host, port, sender, security mode, and credential shape must be validated before an SMTP object or socket is created.
- SMTP outcome
- Connection, TLS, authentication, sender, recipient, and final submission exist only after the configuration gate passes.
Browser-local configuration planner
Find the first broken state boundary
No receiver address, SMTP host, port value, username, password, sender address, message, cookie, authorization header, complete configuration, provider payload, or raw log is requested or transmitted.
- First boundary
- Decision
- Next action
- Exit condition
- Safety rule
What the reviewed Plane paths do
The open Plane issue 9472 reports that Send test email returns HTTP 500 when SMTP fields are filled but not saved. At reviewed commit a8e53b6ac7b87bd8e3e931d21188f7679c7ab6c4, the admin form renders independent Save changes and Send test email buttons. The test button is disabled for an invalid form, but not for a valid dirty form.
The test modal calls sendTestEmail(receiverEmail); it is not passed the current host, port, security mode, sender, username, or password draft. The request therefore tests the saved instance configuration, not the values visible behind the modal.
On the backend, EmailCredentialCheckEndpoint.post() reloads the saved settings and executes port=int(EMAIL_PORT) while constructing the connection. That conversion is above the try beginning at line 120, so ValueError bypasses every bounded SMTP error response.
Make save-before-test explicit and validate before SMTP
For the current receiver-only endpoint, the smallest coherent UI contract is to block the test while isDirty is true. A visible saved-state requirement prevents a successful test of stale settings from being mistaken for proof about the current draft.
The backend remains an independent trust boundary. It should validate the persisted port and other required settings before constructing a Django email connection. Empty, malformed, zero, negative, and greater-than-65535 ports should return a stable 4xx response with no SMTP attempt.
raw_port = str(EMAIL_PORT or "").strip()
try:
port = int(raw_port)
except (TypeError, ValueError):
return Response(
{"error": "Save a numeric SMTP port before sending a test email."},
status=status.HTTP_400_BAD_REQUEST,
)
if not 1 <= port <= 65535:
return Response(
{"error": "SMTP port must be between 1 and 65535."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
connection = get_connection(
host=EMAIL_HOST,
port=port,
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
# Build and send the test message inside the guarded path.
except SMTPAuthenticationError:
# Keep the existing bounded provider-specific response.
...
An alternative draft-test endpoint can be valid, but it is a larger contract: it must validate every draft field server-side, define how an unchanged masked password reuses the stored secret, avoid persisting the draft, and never echo credentials. Mixing the receiver-only endpoint with the visible draft is the unsafe middle state.
Regression matrix
| State | Expected boundary | Proof |
|---|---|---|
| Valid dirty form | No request | Test remains blocked until Save changes succeeds |
| Persisted empty or whitespace port | Configuration 4xx | No get_connection or socket call |
| Persisted nonnumeric, 0, or 65536 port | Configuration 4xx | Stable error shape without raw exception data |
| Saved valid port with bad credentials | SMTP authentication response | Existing bounded invalid-credentials path remains reachable |
| Saved valid configuration | Message submission | HTTP 200 only after msg.send(fail_silently=False) completes |
| Save fails | No request | Test remains blocked and no stale success is displayed |
A successful test is not inbox proof
Once the saved configuration reaches msg.send(), preserve the exact SMTP result. A successful return establishes that Plane completed its configured send path; it does not prove the message became visible in the recipient's inbox. If the endpoint returns 200 but nothing arrives, continue with the provider and recipient handoff guide.
Minimal evidence to keep
- Whether the form was dirty and whether Save changes completed.
- A non-secret configuration revision marker shared by save and test.
- The persisted port category: empty, invalid, or valid. Do not publish the value.
- Whether the request was blocked, returned configuration 4xx, reached SMTP, or returned 200.
- The bounded exception class or provider reply category, without a receiver, credential, message, or full traceback.
This planner runs locally. Do not enter or publish a receiver address, SMTP host, port value, username, password, sender address, message, cookie, authorization header, complete configuration, provider payload, or raw log.