From 1d33222f27290b0281f5c988d15c5bd59f97c4c4 Mon Sep 17 00:00:00 2001 From: Dev9269 <157703342+Dev9269@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:54:09 +0530 Subject: [PATCH] mail: cap an outgoing message at the size the doors already accept One limit on mail.Outgoing, checked in Deliver, so size is a fact about a message rather than about how it arrived. 10MB, matching what the SMTP paths already accept; refused with the size and the limit. Fixes #1465 --- service/mail/deliver.go | 10 ++++++++++ service/mail/deliver_test.go | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/service/mail/deliver.go b/service/mail/deliver.go index 91b14147f..ba5dade35 100644 --- a/service/mail/deliver.go +++ b/service/mail/deliver.go @@ -52,6 +52,13 @@ type Outgoing struct { SenderIP string } +// One limit on an Outgoing, checked here where every door meets, so size +// is a fact about a message rather than about how it arrived. Matches the +// 10MB the SMTP paths already accept: a stored body is re-marshalled, +// re-encrypted and re-written on every later delivery to anybody, so one +// oversized message makes every account slower forever. +const maxOutgoingBytes = 10 << 20 + // Deliver sends one message to wherever its recipient is: off this instance // over SMTP, or into an inbox here. // @@ -65,6 +72,9 @@ func Deliver(m Outgoing) (string, error) { if to == "" { return "", errors.New("no recipient") } + if n := len(m.Body) + len(m.HTML); n > maxOutgoingBytes { + return "", fmt.Errorf("message is %d bytes; the limit is %d", n, maxOutgoingBytes) + } if IsExternalEmail(to) { return ReplyOut(m.FromID, m.Display, to, m.Subject, m.Body, m.HTML, m.InReplyTo, m.References) } diff --git a/service/mail/deliver_test.go b/service/mail/deliver_test.go index 5fe61c0a3..f1cd77829 100644 --- a/service/mail/deliver_test.go +++ b/service/mail/deliver_test.go @@ -1,6 +1,7 @@ package mail import ( + "fmt" "os" "path/filepath" "strings" @@ -264,6 +265,24 @@ func TestDeliverSaysWhoItCouldNotFind(t *testing.T) { } } +// A message bigger than the limit is refused with the size and the limit, +// wherever it came from. A truncated send is worse than a refused one. +// See issue 1465. +func TestDeliverRefusesAnOversizedMessage(t *testing.T) { + t.Setenv("MAIL_DOMAIN", "example.test") + sender := account(t, "sender") + + big := strings.Repeat("x", maxOutgoingBytes) + _, err := Deliver(Outgoing{FromID: sender, Display: "A", To: "someone@example.test", + Subject: "hi", Body: big}) + if err == nil { + t.Fatal("delivered a message over the limit") + } + if !strings.Contains(err.Error(), fmt.Sprintf("%d", maxOutgoingBytes)) { + t.Errorf("error %q does not say the size or the limit", err) + } +} + func account(t *testing.T, id string) string { t.Helper() if have, err := auth.GetAccount(id); err == nil && have != nil {