Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions deploy/env/mta-in-py.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,29 @@ PYMTA_MAX_SESSIONS_TOTAL=2000
PYMTA_MAX_RECIPIENTS=100
PYMTA_MAX_ENVELOPES_PER_CONNECTION=20

# Timeouts (seconds).
# Timeouts (seconds). PYMTA_COMMAND_TIMEOUT is the idle gap allowed between
# complete commands; PYMTA_DATA_TIMEOUT is a separate total budget for the
# whole DATA phase (354 -> last body byte -> MDA deliver -> reply).
PYMTA_COMMAND_TIMEOUT=120
PYMTA_DATA_TIMEOUT=600
PYMTA_DATA_TIMEOUT=300

# STARTTLS off by default in dev (no cert wired).
PYMTA_TLS_CERT_FILE=
PYMTA_TLS_KEY_FILE=

# SMTPUTF8 advertised the MDA accepts UTF-8 envelope addresses.
# SMTPUTF8 advertised: the MDA accepts UTF-8 envelope addresses.
PYMTA_ENABLE_SMTPUTF8=true

# PROXY protocol off in dev. In production set ENABLE_PROXY_PROTOCOL=haproxy
# (same env var the Postfix entrypoint already consumes) when behind HAProxy.
# PROXY protocol off in dev: the test suite connects to pymta directly, so the
# TCP peer IS the client.
#
# Behind a load balancer, set PYMTA_ENABLE_PROXY_PROTOCOL=true AND
# PYMTA_TRUSTED_PROXIES to the balancer's IPs/CIDRs. pymta refuses to start
# with PROXY protocol on and no allowlist. (The Postfix image drives the same
# feature from its own ENABLE_PROXY_PROTOCOL=haproxy; pymta ignores that name.)
#
# A balancer WITHOUT PROXY protocol is not a supported configuration: every
# session would bucket under the balancer's IP and every message would be
# stamped with it as the sender's address.
PYMTA_ENABLE_PROXY_PROTOCOL=false
PYMTA_TRUSTED_PROXIES=
108 changes: 92 additions & 16 deletions src/mta-in/README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/mta-in/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ select = [

[tool.ruff.lint.isort]
section-order = ["future","standard-library","django","third-party","mta","first-party","local-folder"]
sections = { django=["django"] }
sections = { mta=["pymta", "api"], django=["django"] }
extra-standard-library = ["tomllib"]

[tool.ruff.lint.per-file-ignores]
Expand Down
16 changes: 8 additions & 8 deletions src/mta-in/src/delivery_milter.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def envrcpt(self, to, *str):
self.rcpttos.append(clean_to)
return Milter.CONTINUE

except Exception: # noqa: BLE001
except Exception:
# Exception during validation - temporary failure
return Milter.TEMPFAIL

Expand Down Expand Up @@ -150,7 +150,7 @@ def eom(self):
else:
return Milter.TEMPFAIL

except Exception: # noqa: BLE001
except Exception:
return Milter.TEMPFAIL

def close(self):
Expand All @@ -165,7 +165,7 @@ def hello(self, heloname):

def main():
"""Run the milter server"""
print("Starting delivery milter...") # noqa: T201
print("Starting delivery milter...")

# Set the socket for milter communication
# Use Unix socket for better performance and security
Expand All @@ -179,7 +179,7 @@ def main():
try:
os.setgid(grp.getgrnam("postfix").gr_gid)
except (KeyError, OSError) as e:
print(f"Warning: could not set gid to postfix: {e}", file=sys.stderr) # noqa: T201
print(f"Warning: could not set gid to postfix: {e}", file=sys.stderr)
os.umask(0o117)

# Register our milter class
Expand All @@ -189,15 +189,15 @@ def main():
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
Milter.set_flags(flags)

print(f"Milter listening on {socket_path}") # noqa: T201
print(f"Milter listening on {socket_path}")

try:
# Start the milter
Milter.runmilter("delivery_milter", socket_path, timeout=240)
except KeyboardInterrupt:
print("Milter shutting down...") # noqa: T201
except Exception as e: # noqa: BLE001
print(f"Milter error: {e}") # noqa: T201
print("Milter shutting down...")
except Exception as e:
print(f"Milter error: {e}")
sys.exit(1)


Expand Down
4 changes: 2 additions & 2 deletions src/mta-in/src/pymta/address.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""RFC 5321 envelope-address validation.

The functions in this module are intentionally strict: they reject anything
the inbound SMTP server should not have to deal with source routes
the inbound SMTP server should not have to deal with: source routes
(RFC 5321 §4.1.1.3), control characters (CRLF injection vector), overlong
local-parts or domains, and the common ``user@`` / ``@domain`` truncations.

Expand Down Expand Up @@ -104,7 +104,7 @@ def validate_envelope_address( # noqa: PLR0912

# ----- 3. exactly one unquoted '@' ---------------------------------------
# Quoted local-parts could legally contain '@', but we don't accept those
# on the public inbound path — most senders never use them and they are
# on the public inbound path. Most senders never use them and they are
# a fertile parser-confusion ground.
if address.count("@") != 1:
raise AddressError(
Expand Down
4 changes: 2 additions & 2 deletions src/mta-in/src/pymta/controller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""aiosmtpd Controller wired to our :class:`HardenedSMTP` factory.

The Controller itself is unchanged structurally all hardening lives inside
The Controller itself is unchanged structurally; all hardening lives inside
:class:`HardenedSMTP` so the admission gate runs in the same coroutine that
will dispatch SMTP verbs.
"""
Expand Down Expand Up @@ -62,7 +62,7 @@ def build_smtp_kwargs(*, tls_context: ssl.SSLContext | None) -> dict:
def load_tls_context() -> ssl.SSLContext | None:
"""Build a TLS context from the configured cert/key, or None.

Returning None disables STARTTLS aiosmtpd will not advertise it.
Returning None disables STARTTLS, so aiosmtpd will not advertise it.
"""
cert = settings.PYMTA_TLS_CERT_FILE
key = settings.PYMTA_TLS_KEY_FILE
Expand Down
Loading
Loading