From 17346d6def1db7a6fec294afa36232286e9b06fd Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Thu, 22 Aug 2024 21:59:53 +0200 Subject: [PATCH] smtpclient: handle server closing connection after writing its response to RCPT TO if icloud.com has your ip blocklisted, it will close the smtp connection after writing a response to RCPT TO, before writing a response to a pipelined DATA command. this is similar to the case (already handled) where a mail server would close the connection after a response to MAIL FROM when pipelined. we now recognize this situation (unexpected EOF before we get a response to DATA, with all RCPT TO's failed), and treat the last response to RCPT TO as the result. for issue #198 by soheilpro, thanks for reporting and sending an smtpclient trace that showed the behaviour. --- smtpclient/client.go | 9 +++++++++ smtpclient/client_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/smtpclient/client.go b/smtpclient/client.go index 310792d..238f065 100644 --- a/smtpclient/client.go +++ b/smtpclient/client.go @@ -1265,6 +1265,15 @@ func (c *Client) DeliverMultiple(ctx context.Context, mailFrom string, rcptTo [] c.xbotchf(0, "", "", nil, "writing pipelined mail/rcpt/data: %w", writeerr) } + // If remote closed the connection before writing a DATA response, and the RCPT + // TO's failed (e.g. after deciding we're on a blocklist), use the last response + // for a rcptto as result. + if dataerr != nil && errors.Is(dataerr, io.ErrUnexpectedEOF) && nok == 0 { + c.botched = true + r := rcptResps[len(rcptResps)-1] + c.xerrorf(r.Permanent, r.Code, r.Secode, r.Line, r.MoreLines, "%w: server closed connection just before responding to data command", ErrStatus) + } + // If the data command had an i/o or protocol error, it's also a failure for the // entire transaction. if dataerr != nil { diff --git a/smtpclient/client_test.go b/smtpclient/client_test.go index 8b61fcb..4073ae8 100644 --- a/smtpclient/client_test.go +++ b/smtpclient/client_test.go @@ -758,6 +758,33 @@ func TestErrors(t *testing.T) { } }) + // Remote closes connection after 554 response to RCPT TO in pipelined + // connection. Should result in permanent error, not temporary read error. + // E.g. icloud.com that has your IP blocklisted. + run(t, func(s xserver) { + s.writeline("220 mox.example") + s.readline("EHLO") + s.writeline("250-mox.example") + s.writeline("250-ENHANCEDSTATUSCODES") + s.writeline("250 PIPELINING") + s.readline("MAIL FROM:") + s.writeline("250 2.1.0 ok") + s.readline("RCPT TO:") + s.writeline("554 5.7.0 Blocked") + }, func(conn net.Conn) { + c, err := New(ctx, log.Logger, conn, TLSOpportunistic, false, localhost, zerohost, Opts{}) + if err != nil { + panic(err) + } + + msg := "" + err = c.Deliver(ctx, "postmaster@other.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), false, false, false) + var xerr Error + if err == nil || !errors.Is(err, ErrStatus) || !errors.As(err, &xerr) || !xerr.Permanent { + panic(fmt.Errorf("got %#v, expected ErrStatus with Permanent", err)) + } + }) + // If we try multiple recipients and first is 452, it is an error and a // non-pipelined deliver will be aborted. run(t, func(s xserver) {