Skip to content

πŸ”’οΈ(pymta) harden pymta with new settings & limits - #777

Open
sylvinus wants to merge 4 commits into
mainfrom
pymta_review
Open

πŸ”’οΈ(pymta) harden pymta with new settings & limits#777
sylvinus wants to merge 4 commits into
mainfrom
pymta_review

Conversation

@sylvinus

@sylvinus sylvinus commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added configurable session, command-idle, DATA-phase, and JWT timeouts.
    • Added trusted PROXY protocol support with startup validation and safer client-address handling.
    • Improved SMTP session closure, timeout responses, and STARTTLS behavior.
    • Refined delivery outcomes, temporary failures, and permanent rejection handling.
    • Added stricter configuration validation, startup warnings for missing secrets, and optional metrics listener disabling.
  • Bug Fixes

    • Preserved abuse counters across STARTTLS.
    • Corrected EHLO reply formatting and partial-delivery handling.
  • Documentation

    • Expanded production deployment, security, timeout, metrics, and delivery guidance.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 63ea31c1-a8b0-475c-a394-6b8928bd95d4

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between b79cab8 and e33c6a1.

πŸ“’ Files selected for processing (1)
  • src/mta-in/src/pymta/settings.py

πŸ“ Walkthrough

Walkthrough

The pure-Python MTA now validates settings, supports trusted PROXY networks, enforces SMTP session and DATA deadlines, preserves counters across STARTTLS, and applies explicit MDA response and JWT TTL rules. Documentation and tests cover the new behavior.

Changes

SMTP and MDA hardening

Layer / File(s) Summary
Configuration and PROXY startup validation
src/mta-in/src/pymta/settings.py, src/mta-in/src/pymta/server.py, deploy/env/mta-in-py.defaults, src/mta-in/tests/test_settings.py, src/mta-in/tests/test_server_config.py
Settings validate environment values and trusted PROXY networks. JWT TTL, session lifetime, DATA timeout, metrics ports, and error limits are configurable. Startup validates the PROXY trust configuration.
MDA response classification and JWT configuration
src/mta-in/src/pymta/mda_async.py, src/mta-in/tests/test_mda_async.py, src/mta-in/README.md
JWT expiration uses a configurable TTL. HTTP 400, 413, and 415 are permanent delivery rejections. Other non-5xx responses defer, and only 5xx responses affect the circuit breaker.
SMTP deadlines, disconnects, and delivery enforcement
src/mta-in/src/pymta/smtp_protocol.py, src/mta-in/src/pymta/handler.py, src/mta-in/src/pymta/metrics.py, src/mta-in/tests/test_hardened_smtp.py, src/mta-in/tests/test_handler.py
SMTP sessions receive maximum lifetimes. DATA uses a separate deadline. PROXY peers are validated, counters survive STARTTLS, EHLO terminators are repaired, and selected 421 responses close the connection.
Operational documentation and supporting cleanup
src/mta-in/README.md, src/mta-in/src/pymta/address.py, src/mta-in/src/pymta/controller.py, src/mta-in/src/pymta/limits.py, src/mta-in/src/delivery_milter.py, src/mta-in/pyproject.toml, src/mta-in/tests/test_limits.py, src/mta-in/tests/test_security.py
Documentation covers MDA outcomes, proxy deployment, timeout semantics, production settings, rate windows, and address normalization. Lint and comment updates do not change runtime behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SMTPClient
  participant HardenedSMTP
  participant SMTPHandler
  participant MDAClient
  participant Transport
  SMTPClient->>HardenedSMTP: Establish SMTP session
  HardenedSMTP->>Transport: Arm session deadline
  SMTPClient->>SMTPHandler: Send SMTP commands and DATA
  SMTPHandler->>MDAClient: Deliver within remaining DATA budget
  MDAClient-->>SMTPHandler: Return delivery result
  SMTPHandler-->>HardenedSMTP: Return reply and disconnect request
  HardenedSMTP->>Transport: Flush reply and close when requested
Loading

Possibly related PRs

  • suitenumerique/messages#706: Related MTA-to-MDA JWT authentication changes, including configurable JWT TTL and request-body hash binding.
πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (4 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly summarizes the main change: hardening pymta through new settings and limits.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches πŸ’‘ 2
πŸ“ Generate docstrings πŸ’‘
  • Create stacked PR
  • Commit on current branch
πŸ› οΈ Fix failing CI checks πŸ’‘
  • Create stacked PR
  • Commit on current branch

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.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/mta-in/src/pymta/server.py Dismissed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

πŸ€– 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/mta-in/README.md`:
- Around line 49-51: Update the Authorization documentation’s body_hash
description to remove the β€œreplay-proof per-request” claim, stating only that it
binds the token to the exact request bytes. Do not add replay tracking or other
implementation changes unless the documentation is intended to guarantee replay
prevention.

In `@src/mta-in/src/pymta/handler.py`:
- Around line 274-286: Update handle_MAIL() to count malformed sender addresses
and invalid or oversized SIZE parameters, then apply the shared
PYMTA_HARD_ERROR_LIMIT 421-and-disconnect guard before each soft-error return.
Reuse the existing guard behavior from handle_RCPT() and preserve the current
501 responses for individual MAIL validation failures.
- Around line 142-162: Update _remaining_data_budget so it never floors a
depleted budget to one second: after subtracting spent time and
_REPLY_RESERVE_SECONDS from settings.PYMTA_DATA_TIMEOUT, return the actual
nonnegative remaining value or raise TimeoutError when it is nonpositive,
allowing the existing timeout path to send 451 before the DATA deadline.

In `@src/mta-in/src/pymta/mda_async.py`:
- Around line 44-62: Scope _PERMANENT_STATUSES handling in _post to delivery
requests only, so check_recipient responses with 400, 413, or 415 defer instead
of producing temp_fail=False; preserve deferral for other unexpected
recipient-check responses. Update permanent-status tests to exercise deliver and
add check_recipient coverage verifying these statuses defer.

In `@src/mta-in/src/pymta/server.py`:
- Around line 53-59: Update the startup validation guarding
PYMTA_TRUSTED_PROXIES to reject entries representing zero-prefix networks,
including 0.0.0.0/0 and ::/0, in addition to an empty allowlist. Preserve the
existing RuntimeError behavior and ensure validation occurs before the server
accepts connections.

In `@src/mta-in/tests/test_settings.py`:
- Around line 76-79: Update _env_bool so unrecognized non-empty environment
values raise ValueError instead of silently returning the supplied default.
Revise test_bool_unrecognised_value_keeps_the_default to assert the exception
for both default values, while preserving recognized-value parsing and
missing-variable defaults.
πŸͺ„ Autofix

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 Plus

Run ID: 9365f2bf-db5a-4c25-97bd-6bc7bb679c00

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between bfe9492 and 902d52a.

πŸ“’ Files selected for processing (16)
  • deploy/env/mta-in-py.defaults
  • src/mta-in/README.md
  • src/mta-in/src/pymta/address.py
  • src/mta-in/src/pymta/controller.py
  • src/mta-in/src/pymta/handler.py
  • src/mta-in/src/pymta/limits.py
  • src/mta-in/src/pymta/mda_async.py
  • src/mta-in/src/pymta/metrics.py
  • src/mta-in/src/pymta/server.py
  • src/mta-in/src/pymta/settings.py
  • src/mta-in/src/pymta/smtp_protocol.py
  • src/mta-in/tests/test_handler.py
  • src/mta-in/tests/test_hardened_smtp.py
  • src/mta-in/tests/test_mda_async.py
  • src/mta-in/tests/test_server_config.py
  • src/mta-in/tests/test_settings.py

Comment thread src/mta-in/README.md Outdated
Comment thread src/mta-in/src/pymta/handler.py Outdated
Comment thread src/mta-in/src/pymta/handler.py Outdated
Comment thread src/mta-in/src/pymta/mda_async.py
Comment thread src/mta-in/src/pymta/server.py Outdated
Comment thread src/mta-in/tests/test_settings.py Outdated
Comment on lines +76 to +79
def test_bool_unrecognised_value_keeps_the_default(monkeypatch):
monkeypatch.setenv("PYMTA_TEST_BOOL", "maybe")
assert _env_bool("PYMTA_TEST_BOOL", True) is True
assert _env_bool("PYMTA_TEST_BOOL", False) is False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Reject unrecognized Boolean values.

This test requires invalid configuration to fall back silently. It contradicts the fail-fast contract on Lines 3-4.

For example, PYMTA_ENABLE_PROXY_PROTOCOL=treu uses the default False. A deployment behind a PROXY-enabled load balancer then attributes all sessions to the load balancer.

Update _env_bool to raise ValueError, and require that behavior here.

Proposed test change
-def test_bool_unrecognised_value_keeps_the_default(monkeypatch):
+def test_bool_rejects_unrecognised_value(monkeypatch):
     monkeypatch.setenv("PYMTA_TEST_BOOL", "maybe")
-    assert _env_bool("PYMTA_TEST_BOOL", True) is True
-    assert _env_bool("PYMTA_TEST_BOOL", False) is False
+    with pytest.raises(ValueError, match="must be a boolean"):
+        _env_bool("PYMTA_TEST_BOOL", False)
πŸ€– 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/mta-in/tests/test_settings.py` around lines 76 - 79, Update _env_bool so
unrecognized non-empty environment values raise ValueError instead of silently
returning the supplied default. Revise
test_bool_unrecognised_value_keeps_the_default to assert the exception for both
default values, while preserving recognized-value parsing and missing-variable
defaults.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mta-in/README.md (1)

29-42: πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Scope the outcome table to deliver.

The table states that HTTP 400, 413, and 415 always produce 554. check_recipient defers these statuses and the SMTP handler returns 451.

Mark this table as the deliver outcome mapping. Document that recipient checks defer these statuses.

Proposed documentation change
-### Translating the MDA outcome
+### Translating the MDA delivery outcome
+
+For `check/`, HTTP 400, 413, and 415 are temporary failures. They produce
+SMTP `451` because a recipient check contains no message to reject.

-Losing a legitimate message is worse than asking the sender to retry, so the permanent-rejection set is an explicit allow-list and everything else defers:
+For `deliver/`, losing a legitimate message is worse than asking the sender to retry, so the permanent-rejection set is an explicit allow-list and everything else defers:
πŸ€– 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/mta-in/README.md` around lines 29 - 42, Update the β€œTranslating the MDA
outcome” section to explicitly scope its outcome table to deliver responses. Add
documentation stating that check_recipient defers 400, 413, and 415, so its SMTP
handler returns 451 rather than applying the table’s 554 mapping.
πŸ€– 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.

Outside diff comments:
In `@src/mta-in/README.md`:
- Around line 29-42: Update the β€œTranslating the MDA outcome” section to
explicitly scope its outcome table to deliver responses. Add documentation
stating that check_recipient defers 400, 413, and 415, so its SMTP handler
returns 451 rather than applying the table’s 554 mapping.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: de4db120-4f89-4d92-8686-a1726bde25f0

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 902d52a and a42ce31.

πŸ“’ Files selected for processing (13)
  • src/mta-in/README.md
  • src/mta-in/pyproject.toml
  • src/mta-in/src/delivery_milter.py
  • src/mta-in/src/pymta/handler.py
  • src/mta-in/src/pymta/mda_async.py
  • src/mta-in/src/pymta/server.py
  • src/mta-in/src/pymta/settings.py
  • src/mta-in/tests/test_handler.py
  • src/mta-in/tests/test_limits.py
  • src/mta-in/tests/test_mda_async.py
  • src/mta-in/tests/test_security.py
  • src/mta-in/tests/test_server_config.py
  • src/mta-in/tests/test_settings.py

Comment thread src/mta-in/src/pymta/server.py Dismissed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/mta-in/src/pymta/server.py`:
- Around line 56-77: Reject startup in the server configuration validation when
PROXY protocol is enabled and PYMTA_TRUSTED_PROXIES is empty or contains a
zero-prefix network, raising RuntimeError instead of warning. In
src/mta-in/src/pymta/handler.py lines 194-202, return False for an empty
allowlist; update src/mta-in/tests/test_server_config.py lines 26-64 and
src/mta-in/tests/test_handler.py lines 632-644 to assert rejection; document the
explicit trusted proxy IP/CIDR requirement at src/mta-in/README.md line 111 and
remove the unspecified trust-boundary topology at lines 131-140.
πŸͺ„ Autofix

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 Plus

Run ID: a04f3651-5248-4f1b-897c-550c63f8c90b

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between a42ce31 and b79cab8.

πŸ“’ Files selected for processing (7)
  • src/mta-in/README.md
  • src/mta-in/src/pymta/handler.py
  • src/mta-in/src/pymta/mda_async.py
  • src/mta-in/src/pymta/server.py
  • src/mta-in/tests/test_handler.py
  • src/mta-in/tests/test_mda_async.py
  • src/mta-in/tests/test_server_config.py

Comment on lines +56 to +77
if not settings.PYMTA_ENABLE_PROXY_PROTOCOL:
return
# A zero-prefix network (0.0.0.0/0, ::/0) matches every peer, so it is the
# empty allowlist wearing a disguise. Same posture, same warning.
catch_all = [net for net in settings.PYMTA_TRUSTED_PROXIES if net.prefixlen == 0]
if not settings.PYMTA_TRUSTED_PROXIES or catch_all:
why = (
"PYMTA_TRUSTED_PROXIES is empty"
if not settings.PYMTA_TRUSTED_PROXIES
else f"PYMTA_TRUSTED_PROXIES contains {', '.join(str(n) for n in catch_all)}, "
"which matches every peer"
)
logger.warning(
"SECURITY: PROXY protocol is enabled but %s, so a PROXY header is trusted "
"from any peer. Any host able to reach port %s directly can forge its "
"source IP past the per-IP caps and into the Received header. Set it to "
"the load balancer's IPs/CIDRs, and make sure the port is reachable only "
"from the balancer.",
why,
settings.PYMTA_SMTP_PORT,
)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Reject PROXY protocol without a specific trusted-peer allowlist.

An empty or catch-all allowlist authorizes any host that can reach the SMTP listener to forge src_addr. That forged address bypasses per-IP limits and enters the MDA Received metadata. A warning does not establish the required network boundary.

  • src/mta-in/src/pymta/server.py#L56-L77: raise RuntimeError when PYMTA_TRUSTED_PROXIES is empty or contains a zero-prefix network.
  • src/mta-in/src/pymta/handler.py#L194-L202: return False for an empty allowlist as defense in depth.
  • src/mta-in/tests/test_server_config.py#L26-L64: change warning tests to require startup rejection.
  • src/mta-in/tests/test_handler.py#L632-L644: require an empty allowlist to reject the PROXY header.
  • src/mta-in/README.md#L111-L111: state that PROXY mode requires explicit trusted proxy IPs or CIDRs.
  • src/mta-in/README.md#L131-L140: remove the topology that permits an unspecified proxy trust boundary.
🧰 Tools
πŸͺ› GitHub Check: CodeQL

[failure] 74-74: Clear-text logging of sensitive information
This expression logs sensitive data (secret) as clear text.

πŸ“ Affects 5 files
  • src/mta-in/src/pymta/server.py#L56-L77 (this comment)
  • src/mta-in/src/pymta/handler.py#L194-L202
  • src/mta-in/tests/test_server_config.py#L26-L64
  • src/mta-in/tests/test_handler.py#L632-L644
  • src/mta-in/README.md#L111-L111
  • src/mta-in/README.md#L131-L140
πŸ€– 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/mta-in/src/pymta/server.py` around lines 56 - 77, Reject startup in the
server configuration validation when PROXY protocol is enabled and
PYMTA_TRUSTED_PROXIES is empty or contains a zero-prefix network, raising
RuntimeError instead of warning. In src/mta-in/src/pymta/handler.py lines
194-202, return False for an empty allowlist; update
src/mta-in/tests/test_server_config.py lines 26-64 and
src/mta-in/tests/test_handler.py lines 632-644 to assert rejection; document the
explicit trusted proxy IP/CIDR requirement at src/mta-in/README.md line 111 and
remove the unspecified trust-boundary topology at lines 131-140.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants