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 {