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:
Daniel McCarney 2019-01-18 17:14:30 -05:00 committed by Roland Bracewell Shoemaker
parent ed01d6bc14
commit 1a68cc2225
3 changed files with 71 additions and 3 deletions

View File

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

View File

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

View File

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