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.
This commit is contained in:
Mechiel Lukkien 2024-08-22 21:59:53 +02:00
parent c16162eebc
commit 17346d6def
No known key found for this signature in database
2 changed files with 36 additions and 0 deletions

View file

@ -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 {

View file

@ -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) {