Verify the SMTP server's identity, with smtp.verify-server-identity to opt out - #49
Conversation
There was a problem hiding this comment.
🟢 Approval recommended
The change is well-scoped, explicitly documented, and backed by targeted tests covering both TLS modes and the new opt-out behavior.
Pull request overview
This PR makes SMTP server identity verification an explicit Herald behavior (instead of depending on Jakarta Mail defaults) by setting mail.smtp.ssl.checkserveridentity whenever STARTTLS or implicit TLS is used, while adding a new smtp.verify-server-identity config key (default true) to allow opt-out for edge-case relays.
Changes:
- Add
smtp.verify-server-identity(defaulttrue) and plumb it through configuration loading intoEmailNotifier. - Explicitly set
mail.smtp.ssl.checkserveridentitytotrue/falsewhenever an encrypted SMTP mode is enabled, and add a startup warning when verification is opted out. - Add/extend tests and update documentation (CONFIG + user guide + changelog) to describe the behavior change and opt-out.
File summaries
| File | Description |
|---|---|
| USER_GUIDE.md | Documents the new key, the startup warning, and how to recognize hostname/cert mismatch failures. |
| src/test/java/com/dansplugins/herald/HeraldIntegrationTest.java | Ensures new config keys are present in the shipped config.yml. |
| src/test/java/com/dansplugins/herald/EmailNotifierTest.java | Adds session-property assertions and warning behavior tests for server identity verification. |
| src/main/resources/config.yml | Ships the new smtp.verify-server-identity key (default true) with explanatory comments. |
| src/main/java/com/dansplugins/herald/Herald.java | Loads the new config key, passes it to EmailNotifier, and logs a warning when opted out. |
| src/main/java/com/dansplugins/herald/EmailNotifier.java | Adds the new constructor parameter, warning helper, and explicitly sets mail.smtp.ssl.checkserveridentity under TLS modes. |
| CONFIG.md | Adds a dedicated section for smtp.verify-server-identity and clarifies what TLS mode keys do/do not cover. |
| CHANGELOG.md | Records the behavior change and migration/opt-out guidance. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| private EmailNotifier identityNotifier(boolean useTLS, boolean implicitTLS, boolean verifyServerIdentity) { | ||
| return new EmailNotifier("smtp.example.com", 587, "user", "pass", "sender@example.com", | ||
| useTLS, implicitTLS, verifyServerIdentity, Arrays.asList("recipient@example.com"), null, null); | ||
| } |
Self-review rubricScored adversarially against the diff and against command output, on head
Judgment calls left for the reviewer
Out-of-diff observation
This review was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
Merge gate — held for a maintainer decisionEvery mechanical gate is satisfied on head
The merge was nevertheless not taken autonomously, for two reasons. 1. The design decision #48 reserved. That issue states that whether verification is made unconditional or given its own opt-out key is the decision it turns on, and asks for the choice to be made deliberately rather than by precedent. A deliberate choice was made and is argued for in the self-review above — an opt-out, defaulting to strict — but shipping a change that will stop mail from being sent on any installation whose relay presents a certificate that cannot name the address it is dialled at is the maintainer's call to ratify, not this session's. Should unconditional verification be preferred instead, removing the key is a smaller follow-up than adding it after installations have broken. 2. One reviewer comment could not be read. The Copilot review recommends approval and reports one inline comment generated at Lite effort. That comment could not be retrieved in this session, because the API routes that return inline review comments are outside this run's allow-list. It is therefore unread rather than addressed, and is called out as such instead of being silently treated as resolved. This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
…o opt out Disassembling the pinned com.sun.mail:jakarta.mail:2.0.1 shows SocketFetcher reading mail.smtp.ssl.checkserveridentity with a default of false, so neither smtp.use-tls nor smtp.implicit-tls established that the encrypted connection reached the host named in smtp.server. Later versions of the library default the other way, so the property is now set explicitly in both directions whenever an encryption mode is in use, and left unset when neither is. The new smtp.verify-server-identity key defaults to true and exists for the relay whose certificate cannot name the address it is dialled at. Turning it off is warned about at startup, alongside the credential-exposure and port-mismatch warnings it sits with. Closes #48 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A trailing comment cannot align at the column the rest of the smtp block uses, because the key name is longer than that column. Comments above the key are already the file's other shape, used for server-name and for the email subject and body pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
302476c to
90f85a1
Compare
Rebase, re-verification and merge decisionThis PR was found open, carrying the marker of an interrupted session, and was continued rather than duplicated. Brought current with Anchor re-run on the rebased tree. The diff was reviewed again in this session, against the repo's conventions, with nothing blocking found:
The one Copilot inline comment remains unread. The endpoints that return inline review comments are outside this run's allow-list, as they were for the previous session. Copilot's overall verdict is "approval recommended"; the diff was reviewed independently here instead, as above. Merge decision. The previous session held this PR for a maintainer to ratify the opt-out design and the behaviour change. In the three days since, no comment has been left on this PR or on #48. This run is pre-authorized to merge on this repository, and the readiness bar is met: tests green on the exact head, docs verified against the implementation, no path on the do-not-auto-merge list (the This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
Summary
com.sun.mail:jakarta.mail:2.0.1was disassembled (javap -concom.sun.mail.util.SocketFetcher), andmail.smtp.ssl.checkserveridentityis read withiconst_0— a default offalse. Server identity was therefore not being verified under either encryption mode. The relevant bytecode is quoted in the self-review comment on this PR.mail.smtp.ssl.checkserveridentityis now set explicitly, in both directions, wheneversmtp.use-tlsorsmtp.implicit-tlsis in use. Whether the certificate is checked againstsmtp.serveris thereby made a property of Herald rather than of whichever library version happens to be pinned — later releases of the library default the property the other way.smtp.verify-server-identitykey, defaulting totrue, was added rather than the verification being made unconditional. Issue SMTP server identity verification is neither set nor documented #48 names this as the open decision. The opt-out was chosen because a legitimate configuration is affected — a relay dialled by IP address, or one presenting a certificate for another name — which is unlike thesmtp.use-tlsprecedent, where no legitimate reason to opt out of a required STARTTLS existed once the key had been set totrue. Strictness is preserved as the default; the escape hatch is documented rather than being a broken plugin.EmailNotifier.describeUnverifiedServerIdentitywas added and is logged at startup when verification is turned off while an encryption mode is on, following the pattern already set bydescribeCredentialExposureanddescribePortTlsMismatch. It is silent on an unencrypted connection, where there is no certificate to check and the credential-exposure warning already names the larger problem.EmailNotifierconstructors delegate withverifyServerIdentityset totrue, so the secure setting is what a caller gets without asking.implicit-tlswas also added to thetestEveryConfigKeyIsShippedkey list inHeraldIntegrationTest, alongside the new key. Its absence was a pre-existing gap in the same one-line list this change had to edit.Behaviour change
An installation whose relay presents a certificate that cannot name the address it is dialled at will begin to fail its sends after this change, with a certificate error in the log.
CONFIG.md,USER_GUIDE.mdandCHANGELOG.mdeach state this, name the log line it produces, and give both remedies: pointingsmtp.serverat the name the certificate carries, or settingsmtp.verify-server-identitytofalse.Test plan
./gradlew clean build— BUILD SUCCESSFUL./gradlew clean test— 307 tests executed, 0 failures, 0 skipped (count read frombuild/test-results/test/*.xml, not from the banner)checkserveridentityblock reverted, 4 of the new tests fail (Server identity verification should be requested on the STARTTLS path,... on the implicit TLS path,The constructors that predate the setting should verify the identity,Turning verification off should be stated explicitly, not left to the library default); with it restored, all 307 pass.SMTP Session Property Testsand a newServer Identity Verification Testsnest, covering both encryption modes, both settings of the key, the pre-existing constructors, the unencrypted case, and every branch of the new warning.CONFIG.mdcarries asmtp.verify-server-identitysection in the shape of its siblings — type, default, description, worked log line and example — and thesmtp.use-tlsandsmtp.implicit-tlssections now say what they do not cover, which is the misreading issue SMTP server identity verification is neither set nor documented #48 identified.Deferred this cycle
Issue #39 (dms-conventions alignment) was not picked up. It spans twenty-one rubric items across five sections and would exceed this loop's scope ceiling as a single PR, and its first section calls for a
CLAUDE.mdto be authored — agent-loaded configuration, which this loop is required to have authorized separately rather than writing on its own initiative. Splitting it into per-section issues, and an explicit decision on theCLAUDE.mdportion, are both wanted before it is picked up.Closes #48
This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).
drafted by Claude on behalf of Daniel Stephenson