Merge pull request #104 from wneessen/refactor_dsn_to_smtp

Refactor DSN handling from client.go to smtp.go
This commit is contained in:
Winni Neessen 2023-01-18 10:39:21 +01:00 committed by GitHub
commit 91bfed3f3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 45 additions and 114 deletions

View file

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

View file

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

View file

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

View file

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

View file

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