notify-mailer: warn for bad rcpt, don't exit. (#4022)
Resolves https://github.com/letsencrypt/boulder/issues/4019 I can't find RFC verse and chapter for "401 4.1.3" errors, but [IANA's registry of SMTP enhanced status codes](https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml) does show an entry matching `x.1.3`: ``` X.1.3 | Bad destination mailbox address syntax | 501 | The destination address was syntactically invalid. This can apply to any field in the address. This code is only useful for permanent failures. | [RFC3463] (Standards Track) | G. Vaudreuil | IESG ``` However that entry from IANA says the "associated basic code" is 501, not 401. Since we wrote this tool to talk to exactly one SMTP server in the world and it definitely is returning "401 4.1.3" in some cases I think its reasonable to handle as I've done in this PR. Alternative suggestions welcome.
This commit is contained in:
parent
ed01d6bc14
commit
1a68cc2225
|
@ -137,7 +137,13 @@ func (m *mailer) run() error {
|
|||
}
|
||||
err := m.mailer.SendMail([]string{dest}, m.subject, m.emailTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
switch err.(type) {
|
||||
case bmail.InvalidRcptError:
|
||||
m.log.Errf("address %q was rejected by server: %s", dest, err)
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.clk.Sleep(m.sleepInterval)
|
||||
}
|
||||
|
|
|
@ -274,6 +274,16 @@ func (m *MailerImpl) sendOne(to []string, subject, msg string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// InvalidRcptError is returned by SendMail when the server rejects a recipient
|
||||
// address as invalid.
|
||||
type InvalidRcptError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e InvalidRcptError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// SendMail sends an email to the provided list of recipients. The email body
|
||||
// is simple text.
|
||||
func (m *MailerImpl) SendMail(to []string, subject, msg string) error {
|
||||
|
@ -312,6 +322,11 @@ func (m *MailerImpl) SendMail(to []string, subject, msg string) error {
|
|||
m.stats.Inc("SendMail.Errors.SMTP.421", 1)
|
||||
m.reconnect()
|
||||
m.stats.Inc("SendMail.Reconnects", 1)
|
||||
} else if ok && protoErr.Code == 401 && strings.HasPrefix(protoErr.Msg, "4.1.3") {
|
||||
// Error 401 4.1.3 is returned when we send an invalid email address in
|
||||
// a RCPT TO command. Return an identifyable error to the client.
|
||||
m.stats.Inc("SendMail.Errors.SMTP.401", 1)
|
||||
return InvalidRcptError{protoErr.Msg}
|
||||
} else {
|
||||
// If it wasn't an EOF error or a SMTP 421 it is unexpected and we
|
||||
// return from SendMail() with an error
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bufio"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
|
@ -71,7 +70,7 @@ func expect(t *testing.T, buf *bufio.Reader, expected string) error {
|
|||
}
|
||||
if string(line) != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, line)
|
||||
return errors.New("")
|
||||
return fmt.Errorf("Expected %s, got %s", expected, line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -170,6 +169,30 @@ func disconnectHandler(closeFirst int, goodbyeMsg string) connHandler {
|
|||
}
|
||||
}
|
||||
|
||||
func badEmailHandler(messagesToProcess int) connHandler {
|
||||
return func(_ int, t *testing.T, conn net.Conn) {
|
||||
defer func() {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
t.Errorf("conn.Close: %s", err)
|
||||
}
|
||||
}()
|
||||
authenticateClient(t, conn)
|
||||
|
||||
buf := bufio.NewReader(conn)
|
||||
if err := expect(t, buf, "MAIL FROM:<<you-are-a-winner@example.com>> BODY=8BITMIME"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = conn.Write([]byte("250 Sure. Go on. \r\n"))
|
||||
|
||||
if err := expect(t, buf, "RCPT TO:<hi@bye.com>"); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write([]byte("401 4.1.3 Bad recipient address syntax\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func setup(t *testing.T) (*MailerImpl, net.Listener, func()) {
|
||||
stats := metrics.NewNoopScope()
|
||||
fromAddress, _ := mail.ParseAddress("you-are-a-winner@example.com")
|
||||
|
@ -259,6 +282,30 @@ func TestReconnectSuccess(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBadEmailError(t *testing.T) {
|
||||
m, l, cleanUp := setup(t)
|
||||
defer cleanUp()
|
||||
const messages = 3
|
||||
|
||||
go listenForever(l, t, badEmailHandler(messages))
|
||||
|
||||
err := m.Connect()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to connect: %s", err)
|
||||
}
|
||||
|
||||
err = m.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding")
|
||||
// We expect there to be an error
|
||||
if err == nil {
|
||||
t.Errorf("Expected SendMail() to return an InvalidRcptError, got nil")
|
||||
}
|
||||
if rcptErr, ok := err.(InvalidRcptError); !ok {
|
||||
t.Errorf("Expected SendMail() to return an InvalidRcptError, got a %T error: %v", err, err)
|
||||
} else if rcptErr.Message != "4.1.3 Bad recipient address syntax" {
|
||||
t.Errorf("SendMail() returned InvalidRcptError with wrong message: %q\n", rcptErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconnectSMTP421(t *testing.T) {
|
||||
m, l, cleanUp := setup(t)
|
||||
defer cleanUp()
|
||||
|
|
Loading…
Reference in New Issue