From 63d8cef8ca683f80d975798e60a618447ff6a13f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 18 Jan 2023 10:30:06 +0100 Subject: [PATCH] Refactor DSN handling from client.go to smtp.go This PR refactors the the DSN (RFC 1891) SMTP client handling, that was introduced in https://github.com/wneessen/go-mail/commit/f4cdc61dd04afb35e378877f25ec09eff78a18c6. While most of the Client options stay the same, the whole workaround logic for the SMTP client has been removed and added as part of the SMTP client instead. This was we got rid of the Client's own `mail()`, `rcpt()`, `dsnRcpt()`, `dsnMail()` methods as well as the copies of the `cmd()` and `validateLine()` methods. The Client is now using the proper `Mail()` and `Rcpt()` methods of the SMTP client instead. --- client.go | 88 -------------------------------------------------- client_119.go | 13 ++++++-- client_120.go | 12 +++++-- client_test.go | 21 ------------ smtp/smtp.go | 25 +++++++++++++- 5 files changed, 45 insertions(+), 114 deletions(-) diff --git a/client.go b/client.go index 4fa2d26..f0c143a 100644 --- a/client.go +++ b/client.go @@ -633,91 +633,3 @@ func (c *Client) auth() error { } return nil } - -// mail is an extension to the Go std library mail method. It decideds wether to call the -// original mail method from the std library or in case DSN is enabled on the Client to -// call our own method instead -func (c *Client) mail(f string) error { - ok, _ := c.sc.Extension("DSN") - if ok && c.dsn { - return c.dsnMail(f) - } - return c.sc.Mail(f) -} - -// rcpt is an extension to the Go std library rcpt method. It decideds wether to call -// original rcpt method from the std library or in case DSN is enabled on the Client to -// call our own method instead -func (c *Client) rcpt(t string) error { - ok, _ := c.sc.Extension("DSN") - if ok && c.dsn { - return c.dsnRcpt(t) - } - return c.sc.Rcpt(t) -} - -// dsnRcpt issues a RCPT command to the server using the provided email address. -// A call to rcpt must be preceded by a call to mail and may be followed by -// a Data call or another rcpt call. -// -// This is a copy of the original Go std library net/smtp function with additions -// for the DSN extension -func (c *Client) dsnRcpt(t string) error { - if err := validateLine(t); err != nil { - return err - } - if len(c.dsnrntype) <= 0 { - return c.sc.Rcpt(t) - } - - rno := strings.Join(c.dsnrntype, ",") - _, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", t, rno) - return err -} - -// dsnMail issues a MAIL command to the server using the provided email address. -// If the server supports the 8BITMIME extension, mail adds the BODY=8BITMIME -// parameter. If the server supports the SMTPUTF8 extension, mail adds the -// SMTPUTF8 parameter. -// This initiates a mail transaction and is followed by one or more rcpt calls. -// -// This is a copy of the original Go std library net/smtp function with additions -// for the DSN extension -func (c *Client) dsnMail(f string) error { - if err := validateLine(f); err != nil { - return err - } - cmdStr := "MAIL FROM:<%s>" - if ok, _ := c.sc.Extension("8BITMIME"); ok { - cmdStr += " BODY=8BITMIME" - } - if ok, _ := c.sc.Extension("SMTPUTF8"); ok { - cmdStr += " SMTPUTF8" - } - cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype) - - _, _, err := c.cmd(250, cmdStr, f) - return err -} - -// validateLine checks to see if a line has CR or LF as per RFC 5321 -// This is a 1:1 copy of the method from the original Go std library net/smtp -func validateLine(line string) error { - if strings.ContainsAny(line, "\n\r") { - return errors.New("smtp: A line must not contain CR or LF") - } - return nil -} - -// cmd is a convenience function that sends a command and returns the response -// This is a 1:1 copy of the method from the original Go std library net/smtp -func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { - id, err := c.sc.Text.Cmd(format, args...) - if err != nil { - return 0, "", err - } - c.sc.Text.StartResponse(id) - defer c.sc.Text.EndResponse(id) - code, msg, err := c.sc.Text.ReadResponse(expectCode) - return code, msg, err -} diff --git a/client_119.go b/client_119.go index 06053e3..3165a2d 100644 --- a/client_119.go +++ b/client_119.go @@ -7,6 +7,8 @@ package mail +import "strings" + // Send sends out the mail message func (c *Client) Send(ml ...*Msg) error { if cerr := c.checkConn(); cerr != nil { @@ -38,7 +40,12 @@ func (c *Client) Send(ml ...*Msg) error { continue } - if err := c.mail(f); err != nil { + if c.dsn { + if c.dsnmrtype != "" { + c.sc.SetDSNMailReturnOption(string(c.dsnmrtype)) + } + } + if err := c.sc.Mail(f); err != nil { se := &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} if reserr := c.sc.Reset(); reserr != nil { se.errlist = append(se.errlist, reserr) @@ -51,8 +58,10 @@ func (c *Client) Send(ml ...*Msg) error { rse := &SendError{} rse.errlist = make([]error, 0) rse.rcpt = make([]string, 0) + rno := strings.Join(c.dsnrntype, ",") + c.sc.SetDSNRcptNotifyOption(rno) for _, r := range rl { - if err := c.rcpt(r); err != nil { + if err := c.sc.Rcpt(r); err != nil { rse.Reason = ErrSMTPRcptTo rse.errlist = append(rse.errlist, err) rse.rcpt = append(rse.rcpt, r) diff --git a/client_120.go b/client_120.go index 9732f94..69eb5b3 100644 --- a/client_120.go +++ b/client_120.go @@ -9,6 +9,7 @@ package mail import ( "errors" + "strings" ) // Send sends out the mail message @@ -39,7 +40,12 @@ func (c *Client) Send(ml ...*Msg) (rerr error) { continue } - if err := c.mail(f); err != nil { + if c.dsn { + if c.dsnmrtype != "" { + c.sc.SetDSNMailReturnOption(string(c.dsnmrtype)) + } + } + if err := c.sc.Mail(f); err != nil { m.sendError = &SendError{Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err)} rerr = errors.Join(rerr, m.sendError) if reserr := c.sc.Reset(); reserr != nil { @@ -51,8 +57,10 @@ func (c *Client) Send(ml ...*Msg) (rerr error) { rse := &SendError{} rse.errlist = make([]error, 0) rse.rcpt = make([]string, 0) + rno := strings.Join(c.dsnrntype, ",") + c.sc.SetDSNRcptNotifyOption(rno) for _, r := range rl { - if err := c.rcpt(r); err != nil { + if err := c.sc.Rcpt(r); err != nil { rse.Reason = ErrSMTPRcptTo rse.errlist = append(rse.errlist, err) rse.rcpt = append(rse.rcpt, r) diff --git a/client_test.go b/client_test.go index 858019a..56a3879 100644 --- a/client_test.go +++ b/client_test.go @@ -994,27 +994,6 @@ func TestClient_auth(t *testing.T) { } } -// TestValidateLine tests the validateLine() method -func TestValidateLine(t *testing.T) { - tests := []struct { - name string - value string - sf bool - }{ - {"valid line", "valid line", false}, - {`invalid line: \n`, "invalid line\n", true}, - {`invalid line: \r`, "invalid line\r", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validateLine(tt.value); err != nil && !tt.sf { - t.Errorf("validateLine failed: %s", err) - } - }) - } -} - // TestClient_Send_MsgSendError tests the Client.Send method with a broken recipient and verifies // that the SendError type works properly func TestClient_Send_MsgSendError(t *testing.T) { diff --git a/smtp/smtp.go b/smtp/smtp.go index 38408a7..9b5f9aa 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -17,6 +17,7 @@ // 8BITMIME RFC 1652 // AUTH RFC 2554 // STARTTLS RFC 3207 +// DSN RFC 1891 package smtp import ( @@ -50,9 +51,12 @@ type Client struct { localName string // the name to use in HELO/EHLO didHello bool // whether we've said HELO/EHLO helloError error // the error from the hello - + // debug logging debug bool // debug logging is enabled logger *log.Logger // logger will be used for debug logging + // DSN support + dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled + dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled } // logDirection is a type wrapper for the direction a debug log message goes @@ -256,6 +260,10 @@ func (c *Client) Mail(from string) error { if _, ok := c.ext["SMTPUTF8"]; ok { cmdStr += " SMTPUTF8" } + _, ok := c.ext["DSN"] + if ok && c.dsnmrtype != "" { + cmdStr += fmt.Sprintf(" RET=%s", c.dsnmrtype) + } } _, _, err := c.cmd(250, cmdStr, from) return err @@ -268,6 +276,11 @@ func (c *Client) Rcpt(to string) error { if err := validateLine(to); err != nil { return err } + _, ok := c.ext["DSN"] + if ok && c.dsnrntype != "" { + _, _, err := c.cmd(25, "RCPT TO:<%s> NOTIFY=%s", to, c.dsnrntype) + return err + } _, _, err := c.cmd(25, "RCPT TO:<%s>", to) return err } @@ -434,6 +447,16 @@ func (c *Client) SetDebugLog(v bool) { c.logger = nil } +// SetDSNMailReturnOption sets the DSN mail return option for the Mail method +func (c *Client) SetDSNMailReturnOption(d string) { + c.dsnmrtype = d +} + +// SetDSNRcptNotifyOption sets the DSN recipient notify option for the Mail method +func (c *Client) SetDSNRcptNotifyOption(d string) { + c.dsnrntype = d +} + // debugLog checks if the debug flag is set and if so logs the provided message to StdErr func (c *Client) debugLog(d logDirection, f string, a ...interface{}) { if c.debug {