mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-14 09:52:54 +01:00
Merge pull request #47 from wneessen/DSN
Implemented DSNs as described in RFC 1891
This commit is contained in:
commit
f4cdc61dd0
3 changed files with 477 additions and 27 deletions
|
@ -43,7 +43,7 @@ Some of the features of this library:
|
||||||
* [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`)
|
* [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`)
|
||||||
* [X] Support for different encodings
|
* [X] Support for different encodings
|
||||||
* [X] Support sending mails via a local sendmail command
|
* [X] Support sending mails via a local sendmail command
|
||||||
* [X] Support for requestng MDNs
|
* [X] Support for requestng MDNs (RFC 8098) and DSNs (RFC 1891)
|
||||||
* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces
|
* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces
|
||||||
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
|
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
|
||||||
* [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA
|
* [X] Output to file support which allows storing mail messages as e. g. `.eml` files to disk to open them in a MUA
|
||||||
|
|
271
client.go
271
client.go
|
@ -33,14 +33,89 @@ const (
|
||||||
DefaultTLSMinVersion = tls.VersionTLS12
|
DefaultTLSMinVersion = tls.VersionTLS12
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DSNMailReturnOption is a type to define which MAIL RET option is used when a DSN
|
||||||
|
// is requested
|
||||||
|
type DSNMailReturnOption string
|
||||||
|
|
||||||
|
// DSNRcptNotifyOption is a type to define which RCPT NOTIFY option is used when a DSN
|
||||||
|
// is requested
|
||||||
|
type DSNRcptNotifyOption string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DSNMailReturnHeadersOnly requests that only the headers of the message be returned.
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3
|
||||||
|
DSNMailReturnHeadersOnly DSNMailReturnOption = "HDRS"
|
||||||
|
|
||||||
|
// DSNMailReturnFull requests that the entire message be returned in any "failed"
|
||||||
|
// delivery status notification issued for this recipient
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.3
|
||||||
|
DSNMailReturnFull DSNMailReturnOption = "FULL"
|
||||||
|
|
||||||
|
// DSNRcptNotifyNever requests that a DSN not be returned to the sender under
|
||||||
|
// any conditions.
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
|
||||||
|
DSNRcptNotifyNever DSNRcptNotifyOption = "NEVER"
|
||||||
|
|
||||||
|
// DSNRcptNotifySuccess requests that a DSN be issued on successful delivery
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
|
||||||
|
DSNRcptNotifySuccess DSNRcptNotifyOption = "SUCCESS"
|
||||||
|
|
||||||
|
// DSNRcptNotifyFailure requests that a DSN be issued on delivery failure
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
|
||||||
|
DSNRcptNotifyFailure DSNRcptNotifyOption = "FAILURE"
|
||||||
|
|
||||||
|
// DSNRcptNotifyDelay indicates the sender's willingness to receive
|
||||||
|
// "delayed" DSNs. Delayed DSNs may be issued if delivery of a message has
|
||||||
|
// been delayed for an unusual amount of time (as determined by the MTA at
|
||||||
|
// which the message is delayed), but the final delivery status (whether
|
||||||
|
// successful or failure) cannot be determined. The absence of the DELAY
|
||||||
|
// keyword in a NOTIFY parameter requests that a "delayed" DSN NOT be
|
||||||
|
// issued under any conditions.
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891#section-5.1
|
||||||
|
DSNRcptNotifyDelay DSNRcptNotifyOption = "DELAY"
|
||||||
|
)
|
||||||
|
|
||||||
// Client is the SMTP client struct
|
// Client is the SMTP client struct
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
// co is the net.Conn that the smtp.Client is based on
|
||||||
|
co net.Conn
|
||||||
|
|
||||||
|
// Timeout for the SMTP server connection
|
||||||
|
cto time.Duration
|
||||||
|
|
||||||
|
// dsn indicates that we want to use DSN for the Client
|
||||||
|
dsn bool
|
||||||
|
|
||||||
|
// dsnmrtype defines the DSNMailReturnOption in case DSN is enabled
|
||||||
|
dsnmrtype DSNMailReturnOption
|
||||||
|
|
||||||
|
// dsnrntype defines the DSNRcptNotifyOption in case DSN is enabled
|
||||||
|
dsnrntype []string
|
||||||
|
|
||||||
|
// enc indicates if a Client connection is encrypted or not
|
||||||
|
enc bool
|
||||||
|
|
||||||
|
// HELO/EHLO string for the greeting the target SMTP server
|
||||||
|
helo string
|
||||||
|
|
||||||
// Hostname of the target SMTP server cto connect cto
|
// Hostname of the target SMTP server cto connect cto
|
||||||
host string
|
host string
|
||||||
|
|
||||||
|
// pass is the corresponding SMTP AUTH password
|
||||||
|
pass string
|
||||||
|
|
||||||
// Port of the SMTP server cto connect cto
|
// Port of the SMTP server cto connect cto
|
||||||
port int
|
port int
|
||||||
|
|
||||||
|
// sa is a pointer to smtp.Auth
|
||||||
|
sa smtp.Auth
|
||||||
|
|
||||||
|
// satype represents the authentication type for SMTP AUTH
|
||||||
|
satype SMTPAuthType
|
||||||
|
|
||||||
|
// sc is the smtp.Client that is set up when using the Dial*() methods
|
||||||
|
sc *smtp.Client
|
||||||
|
|
||||||
// Use SSL for the connection
|
// Use SSL for the connection
|
||||||
ssl bool
|
ssl bool
|
||||||
|
|
||||||
|
@ -50,32 +125,8 @@ type Client struct {
|
||||||
// tlsconfig represents the tls.Config setting for the STARTTLS connection
|
// tlsconfig represents the tls.Config setting for the STARTTLS connection
|
||||||
tlsconfig *tls.Config
|
tlsconfig *tls.Config
|
||||||
|
|
||||||
// Timeout for the SMTP server connection
|
|
||||||
cto time.Duration
|
|
||||||
|
|
||||||
// HELO/EHLO string for the greeting the target SMTP server
|
|
||||||
helo string
|
|
||||||
|
|
||||||
// enc indicates if a Client connection is encrypted or not
|
|
||||||
enc bool
|
|
||||||
|
|
||||||
// user is the SMTP AUTH username
|
// user is the SMTP AUTH username
|
||||||
user string
|
user string
|
||||||
|
|
||||||
// pass is the corresponding SMTP AUTH password
|
|
||||||
pass string
|
|
||||||
|
|
||||||
// satype represents the authentication type for SMTP AUTH
|
|
||||||
satype SMTPAuthType
|
|
||||||
|
|
||||||
// co is the net.Conn that the smtp.Client is based on
|
|
||||||
co net.Conn
|
|
||||||
|
|
||||||
// sa is a pointer to smtp.Auth
|
|
||||||
sa smtp.Auth
|
|
||||||
|
|
||||||
// sc is the smtp.Client that is set up when using the Dial*() methods
|
|
||||||
sc *smtp.Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option returns a function that can be used for grouping Client options
|
// Option returns a function that can be used for grouping Client options
|
||||||
|
@ -107,6 +158,20 @@ var (
|
||||||
// ErrServerNoUnencoded should be used when 8BIT encoding is selected for a message, but
|
// ErrServerNoUnencoded should be used when 8BIT encoding is selected for a message, but
|
||||||
// the server does not offer 8BITMIME mode
|
// the server does not offer 8BITMIME mode
|
||||||
ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME")
|
ErrServerNoUnencoded = errors.New("message is 8bit unencoded, but server does not support 8BITMIME")
|
||||||
|
|
||||||
|
// ErrInvalidDSNMailReturnOption should be used when an invalid option is provided for the
|
||||||
|
// DSNMailReturnOption in WithDSN
|
||||||
|
ErrInvalidDSNMailReturnOption = errors.New("DSN mail return option can only be HDRS or FULL")
|
||||||
|
|
||||||
|
// ErrInvalidDSNRcptNotifyOption should be used when an invalid option is provided for the
|
||||||
|
// DSNRcptNotifyOption in WithDSN
|
||||||
|
ErrInvalidDSNRcptNotifyOption = errors.New("DSN rcpt notify option can only be: NEVER, " +
|
||||||
|
"SUCCESS, FAILURE or DELAY")
|
||||||
|
|
||||||
|
// ErrInvalidDSNRcptNotifyCombination should be used when an invalid option is provided for the
|
||||||
|
// DSNRcptNotifyOption in WithDSN
|
||||||
|
ErrInvalidDSNRcptNotifyCombination = errors.New("DSN rcpt notify option NEVER cannot be " +
|
||||||
|
"combined with any of SUCCESS, FAILURE or DELAY")
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewClient returns a new Session client object
|
// NewClient returns a new Session client object
|
||||||
|
@ -234,6 +299,72 @@ func WithPassword(p string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDSN enables the Client to request DSNs (if the server supports it)
|
||||||
|
// as described in the RFC 1891 and set defaults for DSNMailReturnOption
|
||||||
|
// to DSNMailReturnFull and DSNRcptNotifyOption to DSNRcptNotifySuccess
|
||||||
|
// and DSNRcptNotifyFailure
|
||||||
|
func WithDSN() Option {
|
||||||
|
return func(c *Client) error {
|
||||||
|
c.dsn = true
|
||||||
|
c.dsnmrtype = DSNMailReturnFull
|
||||||
|
c.dsnrntype = []string{string(DSNRcptNotifyFailure), string(DSNRcptNotifySuccess)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDSNMailReturnType enables the Client to request DSNs (if the server supports it)
|
||||||
|
// as described in the RFC 1891 and set the MAIL FROM Return option type to the
|
||||||
|
// given DSNMailReturnOption
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891
|
||||||
|
func WithDSNMailReturnType(mro DSNMailReturnOption) Option {
|
||||||
|
return func(c *Client) error {
|
||||||
|
switch mro {
|
||||||
|
case DSNMailReturnHeadersOnly:
|
||||||
|
case DSNMailReturnFull:
|
||||||
|
default:
|
||||||
|
return ErrInvalidDSNMailReturnOption
|
||||||
|
}
|
||||||
|
|
||||||
|
c.dsn = true
|
||||||
|
c.dsnmrtype = mro
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDSNRcptNotifyType enables the Client to request DSNs as described in the RFC 1891
|
||||||
|
// and sets the RCPT TO notify options to the given list of DSNRcptNotifyOption
|
||||||
|
// See: https://www.rfc-editor.org/rfc/rfc1891
|
||||||
|
func WithDSNRcptNotifyType(rno ...DSNRcptNotifyOption) Option {
|
||||||
|
return func(c *Client) error {
|
||||||
|
var rnol []string
|
||||||
|
var ns, nns bool
|
||||||
|
if len(rno) > 0 {
|
||||||
|
for _, crno := range rno {
|
||||||
|
switch crno {
|
||||||
|
case DSNRcptNotifyNever:
|
||||||
|
ns = true
|
||||||
|
case DSNRcptNotifySuccess:
|
||||||
|
nns = true
|
||||||
|
case DSNRcptNotifyFailure:
|
||||||
|
nns = true
|
||||||
|
case DSNRcptNotifyDelay:
|
||||||
|
nns = true
|
||||||
|
default:
|
||||||
|
return ErrInvalidDSNRcptNotifyOption
|
||||||
|
}
|
||||||
|
rnol = append(rnol, string(crno))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ns && nns {
|
||||||
|
return ErrInvalidDSNRcptNotifyCombination
|
||||||
|
}
|
||||||
|
|
||||||
|
c.dsn = true
|
||||||
|
c.dsnrntype = rnol
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TLSPolicy returns the currently set TLSPolicy as string
|
// TLSPolicy returns the currently set TLSPolicy as string
|
||||||
func (c *Client) TLSPolicy() string {
|
func (c *Client) TLSPolicy() string {
|
||||||
return c.tlspolicy.String()
|
return c.tlspolicy.String()
|
||||||
|
@ -351,11 +482,11 @@ func (c *Client) Send(ml ...*Msg) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.sc.Mail(f); err != nil {
|
if err := c.mail(f); err != nil {
|
||||||
return fmt.Errorf("sending MAIL FROM command failed: %w", err)
|
return fmt.Errorf("sending MAIL FROM command failed: %w", err)
|
||||||
}
|
}
|
||||||
for _, r := range rl {
|
for _, r := range rl {
|
||||||
if err := c.sc.Rcpt(r); err != nil {
|
if err := c.rcpt(r); err != nil {
|
||||||
return fmt.Errorf("sending RCPT TO command failed: %w", err)
|
return fmt.Errorf("sending RCPT TO command failed: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,3 +638,91 @@ func (c *Client) auth() error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
231
client_test.go
231
client_test.go
|
@ -94,6 +94,13 @@ func TestNewClientWithOptions(t *testing.T) {
|
||||||
false},
|
false},
|
||||||
{"WithUsername()", WithUsername("test"), false},
|
{"WithUsername()", WithUsername("test"), false},
|
||||||
{"WithPassword()", WithPassword("test"), false},
|
{"WithPassword()", WithPassword("test"), false},
|
||||||
|
{"WithDSN()", WithDSN(), false},
|
||||||
|
{"WithDSNMailReturnType()", WithDSNMailReturnType(DSNMailReturnFull), false},
|
||||||
|
{"WithDSNMailReturnType() wrong option", WithDSNMailReturnType("FAIL"), true},
|
||||||
|
{"WithDSNRcptNotifyType()", WithDSNRcptNotifyType(DSNRcptNotifySuccess), false},
|
||||||
|
{"WithDSNRcptNotifyType() wrong option", WithDSNRcptNotifyType("FAIL"), true},
|
||||||
|
{"WithDSNRcptNotifyType() NEVER combination",
|
||||||
|
WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -367,6 +374,87 @@ func TestSetSMTPAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWithDSN tests the WithDSN method for the Client object
|
||||||
|
func TestWithDSN(t *testing.T) {
|
||||||
|
c, err := NewClient(DefaultHost, WithDSN())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.dsn {
|
||||||
|
t.Errorf("WithDSN failed. c.dsn expected to be: %t, got: %t", true, c.dsn)
|
||||||
|
}
|
||||||
|
if c.dsnmrtype != DSNMailReturnFull {
|
||||||
|
t.Errorf("WithDSN failed. c.dsnmrtype expected to be: %s, got: %s", DSNMailReturnFull,
|
||||||
|
c.dsnmrtype)
|
||||||
|
}
|
||||||
|
if c.dsnrntype[0] != string(DSNRcptNotifyFailure) {
|
||||||
|
t.Errorf("WithDSN failed. c.dsnrntype[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
|
||||||
|
c.dsnrntype[0])
|
||||||
|
}
|
||||||
|
if c.dsnrntype[1] != string(DSNRcptNotifySuccess) {
|
||||||
|
t.Errorf("WithDSN failed. c.dsnrntype[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
|
||||||
|
c.dsnrntype[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWithDSNMailReturnType tests the WithDSNMailReturnType method for the Client object
|
||||||
|
func TestWithDSNMailReturnType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value DSNMailReturnOption
|
||||||
|
want string
|
||||||
|
sf bool
|
||||||
|
}{
|
||||||
|
{"WithDSNMailReturnType: FULL", DSNMailReturnFull, "FULL", false},
|
||||||
|
{"WithDSNMailReturnType: HDRS", DSNMailReturnHeadersOnly, "HDRS", false},
|
||||||
|
{"WithDSNMailReturnType: INVALID", "INVALID", "", true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c, err := NewClient(DefaultHost, WithDSNMailReturnType(tt.value))
|
||||||
|
if err != nil && !tt.sf {
|
||||||
|
t.Errorf("failed to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(c.dsnmrtype) != tt.want {
|
||||||
|
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnmrtype))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWithDSNRcptNotifyType tests the WithDSNRcptNotifyType method for the Client object
|
||||||
|
func TestWithDSNRcptNotifyType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value DSNRcptNotifyOption
|
||||||
|
want string
|
||||||
|
sf bool
|
||||||
|
}{
|
||||||
|
{"WithDSNRcptNotifyType: NEVER", DSNRcptNotifyNever, "NEVER", false},
|
||||||
|
{"WithDSNRcptNotifyType: SUCCESS", DSNRcptNotifySuccess, "SUCCESS", false},
|
||||||
|
{"WithDSNRcptNotifyType: FAILURE", DSNRcptNotifyFailure, "FAILURE", false},
|
||||||
|
{"WithDSNRcptNotifyType: DELAY", DSNRcptNotifyDelay, "DELAY", false},
|
||||||
|
{"WithDSNRcptNotifyType: INVALID", "INVALID", "", true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c, err := NewClient(DefaultHost, WithDSNRcptNotifyType(tt.value))
|
||||||
|
if err != nil && !tt.sf {
|
||||||
|
t.Errorf("failed to create new client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(c.dsnrntype) <= 0 && !tt.sf {
|
||||||
|
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
|
||||||
|
}
|
||||||
|
if !tt.sf && c.dsnrntype[0] != tt.want {
|
||||||
|
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnrntype[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
|
// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
|
||||||
func TestSetSMTPAuthCustom(t *testing.T) {
|
func TestSetSMTPAuthCustom(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -589,6 +677,30 @@ func TestClient_DialAndSend(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClient_DialAndSendWithDSN tests the DialAndSend() method of Client with DSN enabled
|
||||||
|
func TestClient_DialAndSendWithDSN(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||||
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
_ = m.FromFormat("go-mail Test Mailer", os.Getenv("TEST_FROM"))
|
||||||
|
_ = m.To(TestRcpt)
|
||||||
|
m.Subject(fmt.Sprintf("This is a test mail from go-mail/v%s", VERSION))
|
||||||
|
m.SetBulk()
|
||||||
|
m.SetDate()
|
||||||
|
m.SetMessageID()
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a test mail from the go-mail library")
|
||||||
|
|
||||||
|
c, err := getTestConnectionWithDSN(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.DialAndSend(m); err != nil {
|
||||||
|
t.Errorf("DialAndSend() failed: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestClient_DialSendCloseBroken tests the Dial(), Send() and Close() method of Client with broken settings
|
// TestClient_DialSendCloseBroken tests the Dial(), Send() and Close() method of Client with broken settings
|
||||||
func TestClient_DialSendCloseBroken(t *testing.T) {
|
func TestClient_DialSendCloseBroken(t *testing.T) {
|
||||||
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||||
|
@ -649,6 +761,67 @@ func TestClient_DialSendCloseBroken(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClient_DialSendCloseBrokenWithDSN tests the Dial(), Send() and Close() method of Client with
|
||||||
|
// broken settings and DSN enabled
|
||||||
|
func TestClient_DialSendCloseBrokenWithDSN(t *testing.T) {
|
||||||
|
if os.Getenv("TEST_ALLOW_SEND") == "" {
|
||||||
|
t.Skipf("TEST_ALLOW_SEND is not set. Skipping mail sending test")
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
from string
|
||||||
|
to string
|
||||||
|
closestart bool
|
||||||
|
closeearly bool
|
||||||
|
sf bool
|
||||||
|
}{
|
||||||
|
{"Invalid FROM", "foo@foo", TestRcpt, false, false, true},
|
||||||
|
{"Invalid TO", os.Getenv("TEST_FROM"), "foo@foo", false, false, true},
|
||||||
|
{"No FROM", "", TestRcpt, false, false, true},
|
||||||
|
{"No TO", os.Getenv("TEST_FROM"), "", false, false, true},
|
||||||
|
{"Close early", os.Getenv("TEST_FROM"), TestRcpt, false, true, true},
|
||||||
|
{"Close start", os.Getenv("TEST_FROM"), TestRcpt, true, false, true},
|
||||||
|
{"Close start/early", os.Getenv("TEST_FROM"), TestRcpt, true, true, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewMsg(WithEncoding(NoEncoding))
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m.SetAddrHeaderIgnoreInvalid(HeaderFrom, tt.from)
|
||||||
|
m.SetAddrHeaderIgnoreInvalid(HeaderTo, tt.to)
|
||||||
|
|
||||||
|
c, err := getTestConnectionWithDSN(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("failed to create test client: %s. Skipping tests", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cfn := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cfn()
|
||||||
|
if err := c.DialWithContext(ctx); err != nil && !tt.sf {
|
||||||
|
t.Errorf("Dail() failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.closestart {
|
||||||
|
_ = c.sc.Close()
|
||||||
|
_ = c.co.Close()
|
||||||
|
}
|
||||||
|
if err := c.Send(m); err != nil && !tt.sf {
|
||||||
|
t.Errorf("Send() failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.closeearly {
|
||||||
|
_ = c.sc.Close()
|
||||||
|
_ = c.co.Close()
|
||||||
|
}
|
||||||
|
if err := c.Close(); err != nil && !tt.sf {
|
||||||
|
t.Errorf("Close() failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// TestClient_auth tests the Dial(), Send() and Close() method of Client with broken settings
|
// TestClient_auth tests the Dial(), Send() and Close() method of Client with broken settings
|
||||||
func TestClient_auth(t *testing.T) {
|
func TestClient_auth(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -687,6 +860,27 @@ 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getTestConnection takes environment variables to establish a connection to a real
|
// getTestConnection takes environment variables to establish a connection to a real
|
||||||
// SMTP server to test all functionality that requires a connection
|
// SMTP server to test all functionality that requires a connection
|
||||||
func getTestConnection(auth bool) (*Client, error) {
|
func getTestConnection(auth bool) (*Client, error) {
|
||||||
|
@ -723,3 +917,40 @@ func getTestConnection(auth bool) (*Client, error) {
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTestConnectionWithDSN takes environment variables to establish a connection to a real
|
||||||
|
// SMTP server to test all functionality that requires a connection. It also enables DSN
|
||||||
|
func getTestConnectionWithDSN(auth bool) (*Client, error) {
|
||||||
|
if os.Getenv("TEST_SKIP_ONLINE") != "" {
|
||||||
|
return nil, fmt.Errorf("env variable TEST_SKIP_ONLINE is set. Skipping online tests")
|
||||||
|
}
|
||||||
|
th := os.Getenv("TEST_HOST")
|
||||||
|
if th == "" {
|
||||||
|
return nil, fmt.Errorf("no TEST_HOST set")
|
||||||
|
}
|
||||||
|
c, err := NewClient(th, WithDSN())
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
if auth {
|
||||||
|
st := os.Getenv("TEST_SMTPAUTH_TYPE")
|
||||||
|
if st != "" {
|
||||||
|
c.SetSMTPAuth(SMTPAuthType(st))
|
||||||
|
}
|
||||||
|
u := os.Getenv("TEST_SMTPAUTH_USER")
|
||||||
|
if u != "" {
|
||||||
|
c.SetUsername(u)
|
||||||
|
}
|
||||||
|
p := os.Getenv("TEST_SMTPAUTH_PASS")
|
||||||
|
if p != "" {
|
||||||
|
c.SetPassword(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.DialWithContext(context.Background()); err != nil {
|
||||||
|
return c, fmt.Errorf("connection to test server failed: %s", err)
|
||||||
|
}
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
return c, fmt.Errorf("disconnect from test server failed: %s", err)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue