🐛(smtp) fix opportunistic TLS against MXes with mismatched certs#687
🐛(smtp) fix opportunistic TLS against MXes with mismatched certs#687sylvinus wants to merge 3 commits into
Conversation
The "may" level was verifying the peer cert and falling back to
cleartext on mismatch, which then bounced on STARTTLS-required
servers (e.g. Mandrill's SES-backed inbound returns 530 to MAIL
FROM in cleartext). Realigned on Postfix's documented behavior:
- "may": opportunistic TLS, no cert verification.
- "secure": mandatory TLS + CA chain + hostname check; defers if
STARTTLS isn't advertised or handshake fails.
- "encrypt" is dropped (replaced by "secure").
Also wires MTA_OUT_SMTP_TLS_SECURITY_LEVEL through both the direct
and relay paths — it had been declared but never read — and
collapses the four proxy_* kwargs + sender_hostname of
send_smtp_mail into a single SmtpProxy dataclass.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughTyped SmtpProxy and settings docs added; send_smtp_mail now accepts a proxy object and smtp_tls_security_level. STARTTLS/TLS behavior moved into helpers with policy-aware fallback/deferral. Outbound direct and relay paths, tests, and docs updated accordingly. ChangesSMTP Proxy and TLS Security Refactoring
Sequence DiagramsequenceDiagram
participant Client as Outbound Caller
participant SendMail as send_smtp_mail
participant Upgrade as _starttls_upgrade
participant Server as SMTP Server
Client->>SendMail: send(message, proxy, level="may")
SendMail->>Upgrade: _starttls_upgrade(level, sender_hostname)
Upgrade->>Server: STARTTLS
Server-->>Upgrade: handshake fails (connection close)
Upgrade-->>SendMail: "fallback"
SendMail->>SendMail: retry with level="none"
SendMail->>Server: deliver plaintext
Server-->>SendMail: accepted
SendMail-->>Client: success
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/backend/core/tests/mda/test_outbound.py (1)
351-392:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the remaining
send_smtp_mailassertions to the new signature.This hunk updates the direct-path expectations, but
test_outbound_send_relayandtest_outbound_send_direct_no_mxstill useassert_called_once_with(...)without the new kwargs. Both will fail now that relay addssmtp_tls_security_level, and the no-proxy direct path also passesproxy=None.As per coding guidelines, "Unit tests should focus on a single use case, keep assertions minimal, and cover all possible cases."Suggested test updates
@@ `@override_settings`( MTA_OUT_MODE="relay", MTA_OUT_RELAY_HOST="smtp.test:1025", # Ensure other auth settings are None for this test MTA_OUT_RELAY_USERNAME="smtp_user", MTA_OUT_RELAY_PASSWORD="smtp_pass", + MTA_OUT_SMTP_TLS_SECURITY_LEVEL="may", OPENSEARCH_INDEX_THREADS=False, ) @@ mock_smtp_send.assert_called_once_with( smtp_host="smtp.test", smtp_port=1025, envelope_from=draft_message.sender.email, recipient_emails={ @@ message_content=draft_message.blob.get_content(), smtp_username="smtp_user", smtp_password="smtp_pass", + smtp_tls_security_level="may", ) @@ `@override_settings`( MTA_OUT_MODE="direct", + MTA_OUT_SMTP_TLS_SECURITY_LEVEL="may", OPENSEARCH_INDEX_THREADS=False, ) @@ mock_smtp_send.assert_called_once_with( smtp_host="example2.com", smtp_ip="1.2.0.8", smtp_port=25, envelope_from=draft_message.sender.email, recipient_emails={"bcc@example2.com"}, message_content=draft_message.blob.get_content(), + smtp_tls_security_level="may", + proxy=None, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/core/tests/mda/test_outbound.py` around lines 351 - 392, The remaining tests use the old send_smtp_mail signature and must be updated to match the new kwargs: update assertions in test_outbound_send_relay and test_outbound_send_direct_no_mx to include smtp_tls_security_level="may" (or the appropriate level for that scenario) and include proxy=None for direct/no-proxy calls; locate the send_smtp_mail mock assertions (e.g., assert_called_once_with(...) in those tests) and add the smtp_tls_security_level and proxy keyword arguments so the expected call signature matches the implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/core/tests/mda/test_smtp.py`:
- Around line 429-452: Add a new unit test that mirrors
test_may_falls_back_when_starttls_handshake_fails but uses
smtp_tls_security_level="secure" to ensure a STARTTLS handshake failure does NOT
fall back to cleartext; instantiate MixedResponseSMTPHandler, set
advertise_starttls = True and starttls_break_handshake = True, start the
handler, call send_smtp_mail with smtp_host, smtp_port=smtp_handler.port,
envelope_from, recipient_emails, message_content, timeout and
smtp_tls_security_level="secure", then assert the delivery for the recipient is
False (or that send_smtp_mail signals failure) and finally stop the handler.
Ensure the test name clearly indicates it verifies secure-mode handshake failure
(e.g., test_secure_fails_on_starttls_handshake_failure) and keeps assertions
minimal.
In `@src/backend/messages/settings.py`:
- Around line 1230-1233: The validation currently rejects the legacy value
"encrypt" for MTA_OUT_SMTP_TLS_SECURITY_LEVEL; update the check to accept
"encrypt" by normalizing it to "secure" and emit a deprecation warning instead
of raising an error. Specifically, in settings.py where
MTA_OUT_SMTP_TLS_SECURITY_LEVEL is validated, detect if its value == "encrypt",
replace/set it to "secure" and call warnings.warn (or processLogger.warning)
with a clear deprecation message advising users to switch to "secure", then
continue the existing membership check against {"none","may","secure"} so
existing deployments do not fail on startup.
---
Outside diff comments:
In `@src/backend/core/tests/mda/test_outbound.py`:
- Around line 351-392: The remaining tests use the old send_smtp_mail signature
and must be updated to match the new kwargs: update assertions in
test_outbound_send_relay and test_outbound_send_direct_no_mx to include
smtp_tls_security_level="may" (or the appropriate level for that scenario) and
include proxy=None for direct/no-proxy calls; locate the send_smtp_mail mock
assertions (e.g., assert_called_once_with(...) in those tests) and add the
smtp_tls_security_level and proxy keyword arguments so the expected call
signature matches the implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: db8771cb-0373-459e-a3fb-d2a30b98c6bf
📒 Files selected for processing (7)
docs/env.mdsrc/backend/core/mda/outbound.pysrc/backend/core/mda/outbound_direct.pysrc/backend/core/mda/smtp.pysrc/backend/core/tests/mda/test_outbound.pysrc/backend/core/tests/mda/test_smtp.pysrc/backend/messages/settings.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/core/tests/mda/test_smtp.py`:
- Around line 454-475: The test test_secure_fails_on_starttls_handshake_failure
currently only asserts delivered is False, which allows permanent failures;
modify the assertion against the send_smtp_mail result for "user1@example.com"
to assert that the delivery was deferred (e.g.
result["user1@example.com"]["deferred"] is True or
result["user1@example.com"]["status"] == "deferred"); if your result shape uses
a different convention, assert the transient nature instead (for example ensure
a "permanent" flag is False or an error_code indicates a temporary/defer) so
MixedResponseSMTPHandler starttls_break_handshake +
smtp_tls_security_level="secure" proves a defer, not a permanent failure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 91750ed0-3688-4078-8715-083de8da7d90
📒 Files selected for processing (2)
src/backend/core/tests/mda/test_outbound.pysrc/backend/core/tests/mda/test_smtp.py
The "may" level was verifying the peer cert and falling back to cleartext on mismatch, which then bounced on STARTTLS-required servers (e.g. Mandrill's SES-backed inbound returns 530 to MAIL FROM in cleartext). Realigned on Postfix's documented behavior:
Also wires MTA_OUT_SMTP_TLS_SECURITY_LEVEL through both the direct and relay paths — it had been declared but never read — and collapses the four proxy_* kwargs + sender_hostname of send_smtp_mail into a single SmtpProxy dataclass.
Summary by CodeRabbit
Documentation
New Features
Bug Fixes
Tests