1
1
Fork 0
mirror of https://github.com/mjl-/mox.git synced 2025-04-21 21:40:01 +03:00

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