fix(relay): raise inbound MaxLineLength from 2000 to 128KiB, add rejected_line_too_long metric - #773
Merged
Merged
Conversation
go-smtp v0.24.0 defaults MaxLineLength to 2000 bytes, and the relay never overrode it. Agent-generated mail — single-line JSON, unfolded HTML, unwrapped base64 — routinely exceeds that, and the relay rejected it at DATA with "554 5.0.0 ... too long a line in input stream". Prod evidence (2026-07-18..20): a customer's two agents (bitswarm-drone/gs@agents.e2a.dev) bounced 30 agent-to-agent messages on exactly this before the customer reformatted their payload. Their use case is the product's core use case. The whole message is already capped by MaxMessageBytes (10MB), so a 1MiB per-line cap bounds per-connection memory without rejecting realistic agent payloads. Adds TestRelay_Data_AcceptsLongLines: delivers a message with a single 64KiB body line through a real socket + DB and asserts it lands in the agent inbox. Verified the test FAILS at the old 2000-byte default and passes with the new cap; full internal/relay suite green. Co-Authored-By: Kimi <noreply@moonshot.ai>
…ne rejects Review findings on the 1MiB cap: MaxLineLength bounds ALL lines including pre-auth SMTP command lines, so it is per-connection memory an unauthenticated peer can force (~1.37MiB live heap per connection at 1MiB, measured); nothing bounds concurrent connections. 128KiB still covers single-line JSON / unfolded HTML with wide margin (64x the old default), stays below the 1MiB header-scan buffer in internal/emailauth/checker.go that the DKIM l= guard depends on, and passes the existing regression test unchanged. Also record a rejected_line_too_long SMTPInbound outcome on the DATA read-error path (errors.Is against go-smtp's ErrTooLongLine sentinel) — the original incident surfaced as a customer complaint because this rejection recorded no metric. Other read errors (client drop, size cap) stay unrecorded as before. Comment no longer claims coverage of unwrapped base64 (a multi-MB attachment line still bounces) and notes #745 already fixed the composer-side source, so this cap guards inbound mail from third-party MTAs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e set Without the prom.go allowlist entry the new outcome would collapse to the "other" label. The duration histogram deliberately keeps its accepted/accepted_dedup/tempfail whitelist — a mid-read abort's elapsed time is not a processing latency and would distort the 2s SLO. Adds the per-outcome metric regression test (554 + exactly one observation) and updates docs/observability.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Raises the inbound relay's
MaxLineLengthfrom go-smtp's 2000-byte default to 128 KiB, records a newrejected_line_too_longSMTP intake metric when a line still exceeds it, and adds regression tests for both.Why
Discovered while investigating a customer's 18% bounce rate on the new delivery-health dashboard: all 30 bounces were
554 5.0.0 Error: transaction failed: smtp: too long a line in input stream— our own relay rejecting agent-to-agent mail at DATA. The customer's two agents (bitswarm-drone/gs@agents.e2a.dev, prod, Jul 18–20) were messaging each other with payloads containing a single >2000-char line — single-line JSON / unfolded HTML, which agent-generated mail produces constantly. They silently retried for three days before reformatting.Review notes (why 128 KiB, not the originally proposed 1 MiB):
MaxLineLengthbounds all lines, including pre-auth SMTP command lines, so it is memory an unauthenticated peer can force per connection (~1.37 MiB live heap/connection measured at 1 MiB; nothing bounds concurrent connections). 128 KiB cuts that exposure 8× while still being 64× the old default — comfortably covering single-line JSON and unfolded HTML.internal/emailauth/checker.go, or the DKIMl=guard there fails open; the constant's comment now records that coupling.io.ReadAllerror path insession.Datareturned before anyrecordSMTPInboundcall. It now recordsrejected_line_too_long(viaerrors.Isagainst go-smtp'sErrTooLongLinesentinel); other DATA read errors (client drop,MaxMessageBytes) deliberately stay unrecorded as before.Changes
internal/relay/server.go—smtpSrv.MaxLineLength = 1 << 17(128 KiB) with a comment recording the prod incident, the command-line/memory bound, theemailauth1 MiB coupling, and the fix(outbound): declare Content-Transfer-Encoding on composed bodies #745 relationship;Data()recordsrejected_line_too_longonErrTooLongLineinternal/telemetry/prom.go/metrics.go— new outcome in thesmtpSetenum allowlist (would otherwise collapse toother); duration histogram keeps its accepted/dedup/tempfail whitelist so a mid-read abort can't distort the 2s latency SLOdocs/observability.md—e2a_smtp_inbound_totaloutcome table updatedinternal/relay/inbound_longline_test.go—TestRelay_Data_AcceptsLongLines: delivers a message with a single 64KiB body line through a real socket + test DB and asserts it lands in the agent inboxinternal/relay/metrics_test.go—TestSMTPInboundMetric_RejectedLineTooLong: a single 160KiB line gets the 554 and records exactly onerejected_line_too_longobservationVerification
TestRelay_Data_AcceptsLongLinesfails at the old 2000-byte default and passes at 128 KiB (verified both directions locally against the test DB)internal/relay+internal/telemetrysuites green;go build ./...,go vet,gofmtclean🤖 Generated with Kimi Code — review findings applied with Claude Code