Imported from StruisICT/smtp-test-tool (
AGENTS.md). Install upstream withnpx skills add StruisICT/smtp-test-tool. Copyright stays with the author.
AGENTS.md — Working agreement for AI coding agents on this repo
Loaded automatically by Claude Code, Cursor, Aider, and most other agent harnesses. Every contributor (human or AI) MUST read this file before changing code. Violations are merge-blockers.
1. Ground rules (hard requirements)
-
Quality over quantity. One feature done well beats three half-baked features. If you cannot finish something to the standard below in the current session, leave it out and open a tracking issue instead.
-
Verify "latest" against live sources. Before adding or upgrading any dependency, language version, GitHub Action, or framework, confirm the current stable release from an authoritative source:
- Rust crates:
cargo search <crate> --limit 1orcurl -s https://crates.io/api/v1/crates/<crate> | jq -r .crate.max_stable_version - GitHub Actions: check the action's repo
Releasespage (orgh release view --repo owner/repo --json tagName). - Rust toolchain:
https://forge.rust-lang.org/infra/channel-layout.htmlorrustup check. - Do not trust prior agent memory for version numbers.
- Rust crates:
-
Accessibility is the bare minimum, not a stretch goal. Every UI surface (desktop GUI, web pages, generated docs site, CLI output) MUST meet WCAG 2.2 Level AAA at a minimum:
- Text contrast ≥ 7:1 against its background (≥ 4.5:1 for large text).
- All information conveyed by colour MUST also have a textual cue
(
[PASS],[FAIL], icons with labels, etc.). Colour is never the only signal. - Full keyboard operability with a visible focus indicator.
- No content flashes more than 3× per second.
- Form fields have visible, programmatic labels (not placeholder-only).
- Live regions / status messages announced to assistive tech (egui ⇒
AccessKit, web ⇒
aria-live). - Honour
prefers-reduced-motionandprefers-contrast.
-
Dark + light mode, on every OS, always. Every UI MUST detect and follow the operating-system appearance setting (Windows registry, macOS
AppleInterfaceStyle, GNOME/KDE/Cosmic, webprefers-color-scheme). A manual override MUST also be available, and the chosen theme MUST persist between sessions. -
No shortcuts, even if they look like overkill. Hand-rolled JSON parser when serde exists? No. Single-file 3000-line module to "save time"? No. The right tool, modular code, real tests, real error handling. If a solution feels too clever, it is wrong.
-
Commit early, commit often, atomic commits. Every logically independent change is its own commit with a Conventional Commits message (
feat:,fix:,chore:,docs:,refactor:,test:,ci:,perf:). Never bundle unrelated changes. This is what lets usgit revertcleanly when something breaks. Push to a feature branch, open a PR, let CI run; merge only when green. -
Polish counts. GUI spacing, web typography, CLI output alignment, error message wording — all of it is part of the product. If it looks amateur, it is broken.
-
Credentials never touch the config file, logs, or eframe state. Passwords, OAuth bearer tokens, API keys, and similar secrets live in process memory for the current session only — OR in an OS keychain via
src/keystore.rs, which is the one approved persistent store. The OS keychain provides real at-rest encryption gated by the OS login / unlock prompt; our own TOML files do not. Enforced at the type level via#[serde(skip)]onProfile::passwordand::oauth_token, behaviourally bytests/config_roundtrip.rs::save_never_writes_credentials_even_when_set, and the keychain code itself is feature-gated so a CLI-only build can ship without ever linking the keyring crate. Removing or weakening any of those is a merge blocker.
2. Stack of record (so agents don't churn it)
| Layer | Choice | Why |
|---|---|---|
| Language | Rust (edition 2021) | Safety, single static binary, modern tooling. |
| MSRV | 1.92 | Floor set by the egui 0.34 ecosystem. |
| TLS | rustls + ring |
Pure Rust, no OpenSSL on host. |
| SMTP | lettre 0.11+ |
De-facto Rust SMTP client. |
| IMAP / POP3 | hand-rolled on rustls | Owns the wire trace for diagnostics. |
| CLI parsing | clap 4 derive |
Standard. |
| Config | serde + toml |
Human-editable, IT-friendly. |
| Logging | tracing family |
One subscriber, many sinks (CLI, GUI, file). |
| Desktop GUI | eframe/egui |
Single binary, AccessKit, OS theme follow. |
| Web (if needed) | not yet decided | When added: must meet rule #3 from day one. |
Before changing any of the above, open an issue with rationale; never silently swap.
3. Definition of Done for any change
A pull request is only ready to merge when all of these are true:
- Builds clean on Linux + macOS + Windows in CI.
-
cargo fmt --all -- --checkpasses. -
cargo clippy --all-targets --all-features -- -D warningspasses. -
cargo test --all-featurespasses. -
cargo deny checkpasses (advisories, licenses, sources, bans). - If user-facing: screenshots in dark and light mode attached to the PR, plus a paragraph describing the keyboard path through the new UI.
- If protocol-affecting: example real-world server diagnostic added
to the
#[cfg(test)]module insrc/diagnostics.rs(the diagnostics fixtures live inline with the translator they exercise, not in a separatetests/integration file). -
CHANGELOG.mdupdated under## [Unreleased]. - No
unwrap()/expect()in non-test code without a// SAFETY:-style comment justifying it. - No new dependency added without verifying it is the latest stable
(rule #2) and that
cargo denyaccepts its licence.
4. Commit / branch workflow
main ← protected, always green, always shippable
└── feat/x ← short-lived branches, squash-merge via PR
- One PR = one concern.
- Commit message body explains why, not what (the diff shows what).
- Reference issues with
Refs #NorCloses #N. - Tag releases with
vX.Y.Z; CI then builds and publishes binaries + the crate to crates.io.
5. When you (an AI agent) are blocked
- Do not invent API surfaces. Read the actual crate docs (
cargo doc --openor docs.rs) before guessing. - If a build fails, paste the exact error in your reply and fix the smallest possible thing first — do not refactor under cover of a bug fix.
- If you broke something,
git statusandgit diffbefore doing anything else. If unsure,git stashand ask the user. - Tell the user the truth, including "I can't verify X right now because Y". Do not bluff.
6. Versioning (SemVer 2.0.0)
This project follows Semantic Versioning 2.0.0
to the letter. Versions are MAJOR.MINOR.PATCH, optionally with a
-prerelease and/or +build suffix. Cargo.toml is the single
source of truth for the number; the git tag is the same value with a
v prefix (vX.Y.Z).
What counts as the public API
A change is breaking if it breaks any of these contracts for an existing user:
- Library surface — anything reachable from
smtp_test_tool::(thepubitems re-exported insrc/lib.rs): types, fields, function signatures, enum variants, trait impls, and the set of Cargo feature names. - CLI contract — subcommand names, flag/argument names and their
meaning, and the documented exit codes (
0pass,1fail,2config/internal error). - Config + persistence schema — the TOML keys in
smtp_test_tool.tomland their semantics, plus the OS-keychain entry naming.
Internal modules, private items, log wording, and the exact text of diagnostic hints are not part of the public API and may change in any release.
Choosing the bump
- MAJOR — remove or rename a public item, change a function signature or a CLI flag, change an exit code, drop a config key, or any other backward-incompatible change.
- MINOR — add a public item, CLI subcommand/flag, config key, protocol, provider preset, locale, or Cargo feature in a backward-compatible way (also: mark something deprecated without removing it).
- PATCH — a backward-compatible bug fix only; no new public surface.
0.y.z (we are here)
While the version is 0.y.z the public API is not stable
(spec §4). Our self-imposed discipline during 0.x:
- A breaking change bumps the MINOR:
0.2.x→0.3.0. - A backward-compatible feature or fix bumps the PATCH:
0.2.0→0.2.1. 1.0.0is the first release that commits to a stable API. Do not cut it until the library surface, CLI, and config schema are ones we are willing to keep stable.
Pre-releases and build metadata
- Pre-release identifiers (
1.0.0-rc.1,0.3.0-beta.2) are dot-separated alphanumerics and rank below the matching release. A tag containing a-is published as a GitHub prerelease automatically (release.yml). - Build metadata (
+…) is permitted by the spec but we do not use it; it is ignored for precedence. - MSRV is orthogonal to SemVer. Raising the Rust floor (now 1.92) is recorded in the CHANGELOG and, for the published library, treated as at least a MINOR bump.
Release procedure
- Pick the bump using the rules above.
- Edit
versioninCargo.toml; runcargo buildsoCargo.lockupdates its own package entry too. - In
CHANGELOG.md, rename## [Unreleased]to## [X.Y.Z] - YYYY-MM-DDand open a fresh empty## [Unreleased]above it. - Commit
chore(release): bump to X.Y.Z. git tag -a vX.Y.Z -m "vX.Y.Z"and push the tag. CI gates the release on tag ==Cargo.toml== a matchingCHANGELOGsection, then builds binaries, publishes, and refreshes the package manifests.
7. Current state & resume context
Snapshot for whoever (human or AI) picks this up next — on a fresh PC, a different tool, or weeks later. Keep this section current: when you ship something here, update the "Shipped" list, bump the version note, and re-prioritise "Next up" before you finish a session. Treat a stale snapshot here as a bug.
Where things live
- Repo / org:
github.com/StruisICT/smtp-test-tool(moved fromStruis112; old URLs redirect for ~12 months). - Crate:
smtp-test-toolon crates.io (lib namesmtp_test_tool). - Binaries:
smtp-test-tool(CLI) +smtp-test-tool-gui(GUI,guifeature). Single static binary per OS, no host OpenSSL. - Package channels: WinGet (
StruisICT.SmtpTestTool), Scoop (struisictbucket), Homebrew (struisict/tap). Manifests inpackaging/, auto-refreshed by.github/workflows/release.yml. - Default features:
gui,keychain,dns,oauth.
Shipped (as of v0.2.1)
- SMTP (lettre) + hand-rolled IMAP / POP3 over rustls, full wire trace.
- IT-actionable diagnostics (M365 error-code translation).
- 11 provider presets; TOML profiles; OS-keychain credential storage.
- DNS audit (MX / SPF / DMARC / DKIM + hints), CLI
dns+ GUI DNS check. DKIM (v0.2.1): selector-driven probing with aCOMMON_DKIM_SELECTORSfallback, RSA key-strength via a DER walk of thep=SPKI, hints for revoked / weak / 1024-bit / testing-mode keys; CLI--dkim-selector/--no-dkim, GUI selectors field. - M365 OAuth2 device-code flow (RFC 8628), CLI
oauth-login+ GUI. - 36 locales / 11 scripts, OS dark/light follow, WCAG 2.2 AAA, AccessKit.
In flight
- WinGet re-submission —
microsoft/winget-pkgs#400429(v0.2.1, under theStruisICTpublisher) is open and awaiting the Microsoft validation bot + moderator approval. Submitted from theStruis112/winget-pkgsfork; hash/URL/layout verified andwinget validatepassed. The withdrawn 0.2.0 PR is superseded. If the bot asks for a Sandbox install test, that step still needs a Windows desktop (this host is a headless Server build).
Next up (suggested order — confirm with the maintainer)
- More DNS diagnostics: MTA-STS / TLS-RPT / BIMI (each a natural
follow-up to the SPF/DMARC/DKIM audit in
src/dns.rs). - Translate the 9 English-only DNS/OAuth UI strings tracked in
i18n.rs::PENDING_TRANSLATIONand shrink that allowlist. - Consider migrating
fontdb→skrifa/alternative to drop the unmaintainedttf-parser(currently a documenteddeny.tomlignore for RUSTSEC-2026-0192, alongside the two build-time-onlyquick-xmlDoS advisories 0194/0195; seedeny.tomlfor the rationale + revisit conditions).
Verify-green checklist (run before any commit)
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo deny check
GUI screenshots regenerate via tools/ (see tools/README.md) and
must be attached for any user-facing change, in both themes.