diff --git a/internal/smtpconn/smtpconn.go b/internal/smtpconn/smtpconn.go index 7ec93cac..50d91a0e 100644 --- a/internal/smtpconn/smtpconn.go +++ b/internal/smtpconn/smtpconn.go @@ -79,6 +79,13 @@ type C struct { // "ADDRESS said: ..." AddrInSMTPMsg bool + // Treat DNS resolution errors returned while connecting to the configured + // endpoint as temporary delivery errors. + // + // This is useful for explicitly configured downstream SMTP/LMTP targets where + // the endpoint is mail infrastructure, not recipient-domain DNS. + DNSErrorsTemporary bool + conn net.Conn serverName string cl *smtp.Client @@ -135,9 +142,17 @@ func (c *C) wrapClientErr(err error, serverName string) error { reason, misc := exterrors.UnwrapDNSErr(err) misc["remote_server"] = err.Addr misc["io_op"] = err.Op + + code := exterrors.SMTPCode(err, 450, 550) + enhancedCode := exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}) + if c.DNSErrorsTemporary { + code = 451 + enhancedCode = exterrors.EnhancedCode{4, 4, 4} + } + return &exterrors.SMTPError{ - Code: exterrors.SMTPCode(err, 450, 550), - EnhancedCode: exterrors.SMTPEnchCode(err, exterrors.EnhancedCode{0, 4, 4}), + Code: code, + EnhancedCode: enhancedCode, Message: "DNS error", Err: err, Reason: reason, diff --git a/internal/smtpconn/smtpconn_dns_test.go b/internal/smtpconn/smtpconn_dns_test.go new file mode 100644 index 00000000..bfddf649 --- /dev/null +++ b/internal/smtpconn/smtpconn_dns_test.go @@ -0,0 +1,90 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package smtpconn + +import ( + "errors" + "net" + "testing" + + "github.com/foxcpp/maddy/framework/exterrors" +) + +func dnsOpError() error { + return &net.OpError{ + Op: "dial", + Net: "tcp", + Addr: &net.TCPAddr{ + IP: net.ParseIP("192.0.2.1"), + Port: 25, + }, + Err: &net.DNSError{ + Err: "no such host", + Name: "smtp.example.invalid", + IsNotFound: true, + }, + } +} + +func TestWrapClientErrDNSErrorDefaultClassification(t *testing.T) { + c := New() + + err := c.wrapClientErr(dnsOpError(), "smtp.example.invalid") + + var smtpErr *exterrors.SMTPError + if !errors.As(err, &smtpErr) { + t.Fatalf("expected *exterrors.SMTPError, got %T: %v", err, err) + } + + if smtpErr.Code != 550 { + t.Fatalf("unexpected SMTP code: got %d, want 550", smtpErr.Code) + } + + if smtpErr.EnhancedCode != (exterrors.EnhancedCode{5, 4, 4}) { + t.Fatalf("unexpected enhanced SMTP code: got %v, want 5.4.4", smtpErr.EnhancedCode) + } + + if smtpErr.Temporary() { + t.Fatal("default DNS error classification should be permanent") + } +} + +func TestWrapClientErrDNSErrorTemporaryClassification(t *testing.T) { + c := New() + c.DNSErrorsTemporary = true + + err := c.wrapClientErr(dnsOpError(), "smtp.example.invalid") + + var smtpErr *exterrors.SMTPError + if !errors.As(err, &smtpErr) { + t.Fatalf("expected *exterrors.SMTPError, got %T: %v", err, err) + } + + if smtpErr.Code != 451 { + t.Fatalf("unexpected SMTP code: got %d, want 451", smtpErr.Code) + } + + if smtpErr.EnhancedCode != (exterrors.EnhancedCode{4, 4, 4}) { + t.Fatalf("unexpected enhanced SMTP code: got %v, want 4.4.4", smtpErr.EnhancedCode) + } + + if !smtpErr.Temporary() { + t.Fatal("DNS error should be temporary when DNSErrorsTemporary is enabled") + } +} diff --git a/internal/target/smtp/smtp_downstream.go b/internal/target/smtp/smtp_downstream.go index 5fbbd751..823b1032 100644 --- a/internal/target/smtp/smtp_downstream.go +++ b/internal/target/smtp/smtp_downstream.go @@ -224,6 +224,7 @@ func (d *delivery) connect(ctx context.Context) error { conn.Log = d.log conn.Hostname = d.u.hostname conn.AddrInSMTPMsg = false + conn.DNSErrorsTemporary = true if d.u.connectTimeout != 0 { conn.ConnectTimeout = d.u.connectTimeout }