Compare commits

..

1 commit

Author SHA1 Message Date
Michael Fuchs
e9f7be7dce
Merge 894936092e into 619318ba0d 2024-09-20 11:33:59 +02:00
13 changed files with 45 additions and 842 deletions

View file

@ -36,7 +36,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.19', '1.20', '1.23'] go: ['1.20', '1.21', '1.22', '1.23']
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1

View file

@ -7,22 +7,16 @@
package mail package mail
import "errors"
// Send sends out the mail message // Send sends out the mail message
func (c *Client) Send(messages ...*Msg) error { func (c *Client) Send(messages ...*Msg) error {
if err := c.checkConn(); err != nil { if err := c.checkConn(); err != nil {
return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)}
} }
var errs []*SendError var errs []*SendError
for id, message := range messages { for _, message := range messages {
if sendErr := c.sendSingleMsg(message); sendErr != nil { if sendErr := c.sendSingleMsg(message); sendErr != nil {
messages[id].sendError = sendErr messages[id].sendError = sendErr
errs = append(errs, sendErr)
var msgSendErr *SendError
if errors.As(sendErr, &msgSendErr) {
errs = append(errs, msgSendErr)
}
} }
} }

View file

@ -5,7 +5,6 @@
package mail package mail
import ( import (
"bufio"
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
@ -22,18 +21,11 @@ import (
"github.com/wneessen/go-mail/smtp" "github.com/wneessen/go-mail/smtp"
) )
const (
// DefaultHost is used as default hostname for the Client // DefaultHost is used as default hostname for the Client
DefaultHost = "localhost" const DefaultHost = "localhost"
// TestRcpt is a trash mail address to send test mails to
TestRcpt = "go-mail@mytrashmailer.com" // TestRcpt
// TestServerProto is the protocol used for the simple SMTP test server const TestRcpt = "go-mail@mytrashmailer.com"
TestServerProto = "tcp"
// TestServerAddr is the address the simple SMTP test server listens on
TestServerAddr = "127.0.0.1"
// TestServerPortBase is the base port for the simple SMTP test server
TestServerPortBase = 2025
)
// TestNewClient tests the NewClient() method with its default options // TestNewClient tests the NewClient() method with its default options
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
@ -1259,475 +1251,6 @@ func TestClient_DialAndSendWithContext_withSendError(t *testing.T) {
} }
} }
func TestClient_SendErrorNoEncoding(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
featureSet := "250-AUTH PLAIN\r\n250-DSN\r\n250 SMTPUTF8"
serverPort := TestServerPortBase + 1
go func() {
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("valid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "Test body")
message.SetMessageIDWithValue("this.is.a.message.id")
message.SetEncoding(NoEncoding)
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrNoUnencoded {
t.Errorf("expected ErrNoUnencoded error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
if sendErr.Msg() == nil {
t.Errorf("expected message to be set, but got nil")
}
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
}
func TestClient_SendErrorMailFrom(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 2
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("invalid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("valid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "Test body")
message.SetMessageIDWithValue("this.is.a.message.id")
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrSMTPMailFrom {
t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
if sendErr.Msg() == nil {
t.Errorf("expected message to be set, but got nil")
}
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
}
func TestClient_SendErrorMailFromReset(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 3
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("invalid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("valid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "Test body")
message.SetMessageIDWithValue("this.is.a.message.id")
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrSMTPMailFrom {
t.Errorf("expected ErrSMTPMailFrom error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
if len(sendErr.errlist) != 2 {
t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist))
return
}
if !strings.EqualFold(sendErr.errlist[0].Error(), "503 5.1.2 Invalid from: <invalid-from@domain.tld>") {
t.Errorf("expected error: %q, but got %q",
"503 5.1.2 Invalid from: <invalid-from@domain.tld>", sendErr.errlist[0].Error())
}
if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") {
t.Errorf("expected error: %q, but got %q",
"500 5.1.2 Error: reset failed", sendErr.errlist[1].Error())
}
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
}
func TestClient_SendErrorToReset(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 4
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("invalid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "Test body")
message.SetMessageIDWithValue("this.is.a.message.id")
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrSMTPRcptTo {
t.Errorf("expected ErrSMTPRcptTo error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
if len(sendErr.errlist) != 2 {
t.Errorf("expected 2 errors, but got %d", len(sendErr.errlist))
return
}
if !strings.EqualFold(sendErr.errlist[0].Error(), "500 5.1.2 Invalid to: <invalid-to@domain.tld>") {
t.Errorf("expected error: %q, but got %q",
"500 5.1.2 Invalid to: <invalid-to@domain.tld>", sendErr.errlist[0].Error())
}
if !strings.EqualFold(sendErr.errlist[1].Error(), "500 5.1.2 Error: reset failed") {
t.Errorf("expected error: %q, but got %q",
"500 5.1.2 Error: reset failed", sendErr.errlist[1].Error())
}
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
}
func TestClient_SendErrorDataClose(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 5
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("valid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "DATA close should fail")
message.SetMessageIDWithValue("this.is.a.message.id")
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrSMTPDataClose {
t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
}
}
func TestClient_SendErrorDataWrite(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 6
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("valid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "DATA write should fail")
message.SetMessageIDWithValue("this.is.a.message.id")
message.SetGenHeader("X-Test-Header", "DATA write should fail")
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrSMTPDataClose {
t.Errorf("expected ErrSMTPDataClose error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
}
}
func TestClient_SendErrorReset(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverPort := TestServerPortBase + 7
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, true, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
message := NewMsg()
if err := message.From("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set FROM address: %s", err)
return
}
if err := message.To("valid-to@domain.tld"); err != nil {
t.Errorf("failed to set TO address: %s", err)
return
}
message.Subject("Test subject")
message.SetBodyString(TypeTextPlain, "Test body")
message.SetMessageIDWithValue("this.is.a.message.id")
client, err := NewClient(TestServerAddr, WithPort(serverPort),
WithTLSPortPolicy(NoTLS), WithSMTPAuth(SMTPAuthPlain),
WithUsername("toni@tester.com"),
WithPassword("V3ryS3cr3t+"))
if err != nil {
t.Errorf("unable to create new client: %s", err)
}
if err = client.DialWithContext(context.Background()); err != nil {
t.Errorf("failed to dial to test server: %s", err)
}
if err = client.Send(message); err == nil {
t.Error("expected Send() to fail but didn't")
}
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Errorf("expected *SendError type as returned error, but got %T", sendErr)
}
if errors.As(err, &sendErr) {
if sendErr.IsTemp() {
t.Errorf("expected permanent error but IsTemp() returned true")
}
if sendErr.Reason != ErrSMTPReset {
t.Errorf("expected ErrSMTPReset error, but got %s", sendErr.Reason)
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("expected message ID: %q, but got %q", "<this.is.a.message.id>",
sendErr.MessageID())
}
}
if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %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) {
@ -2022,155 +1545,3 @@ func (f faker) RemoteAddr() net.Addr { return nil }
func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetDeadline(time.Time) error { return nil }
func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil }
func (f faker) SetWriteDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil }
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
// The provided featureSet represents in what the server responds to EHLO command
// failReset controls if a RSET succeeds
func simpleSMTPServer(ctx context.Context, featureSet string, failReset bool, port int) error {
listener, err := net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, port))
if err != nil {
return fmt.Errorf("unable to listen on %s://%s: %w", TestServerProto, TestServerAddr, err)
}
defer func() {
if err := listener.Close(); err != nil {
fmt.Printf("unable to close listener: %s\n", err)
os.Exit(1)
}
}()
for {
select {
case <-ctx.Done():
return nil
default:
connection, err := listener.Accept()
var opErr *net.OpError
if err != nil {
if errors.As(err, &opErr) && opErr.Temporary() {
continue
}
return fmt.Errorf("unable to accept connection: %w", err)
}
handleTestServerConnection(connection, featureSet, failReset)
}
}
}
func handleTestServerConnection(connection net.Conn, featureSet string, failReset bool) {
defer func() {
if err := connection.Close(); err != nil {
fmt.Printf("unable to close connection: %s\n", err)
}
}()
reader := bufio.NewReader(connection)
writer := bufio.NewWriter(connection)
writeLine := func(data string) error {
_, err := writer.WriteString(data + "\r\n")
if err != nil {
return fmt.Errorf("unable to write line: %w", err)
}
return writer.Flush()
}
writeOK := func() {
_ = writeLine("250 2.0.0 OK")
}
if err := writeLine("220 go-mail test server ready ESMTP"); err != nil {
fmt.Printf("unable to write to client: %s\n", err)
return
}
data, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("unable to read from connection: %s\n", err)
return
}
if !strings.HasPrefix(data, "EHLO") && !strings.HasPrefix(data, "HELO") {
fmt.Printf("expected EHLO, got %q", data)
return
}
if err = writeLine("250-localhost.localdomain\r\n" + featureSet); err != nil {
fmt.Printf("unable to write to connection: %s\n", err)
return
}
for {
data, err = reader.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
break
}
fmt.Println("Error reading data:", err)
break
}
var datastring string
data = strings.TrimSpace(data)
switch {
case strings.HasPrefix(data, "MAIL FROM:"):
from := strings.TrimPrefix(data, "MAIL FROM:")
from = strings.ReplaceAll(from, "BODY=8BITMIME", "")
from = strings.ReplaceAll(from, "SMTPUTF8", "")
from = strings.TrimSpace(from)
if !strings.EqualFold(from, "<valid-from@domain.tld>") {
_ = writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from))
break
}
writeOK()
case strings.HasPrefix(data, "RCPT TO:"):
to := strings.TrimPrefix(data, "RCPT TO:")
to = strings.TrimSpace(to)
if !strings.EqualFold(to, "<valid-to@domain.tld>") {
_ = writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to))
break
}
writeOK()
case strings.HasPrefix(data, "AUTH PLAIN"):
auth := strings.TrimPrefix(data, "AUTH PLAIN ")
if !strings.EqualFold(auth, "AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") {
_ = writeLine("535 5.7.8 Error: authentication failed")
break
}
_ = writeLine("235 2.7.0 Authentication successful")
case strings.EqualFold(data, "DATA"):
_ = writeLine("354 End data with <CR><LF>.<CR><LF>")
for {
ddata, derr := reader.ReadString('\n')
if derr != nil {
fmt.Printf("failed to read DATA data from connection: %s\n", derr)
break
}
ddata = strings.TrimSpace(ddata)
if strings.EqualFold(ddata, "DATA write should fail") {
_ = writeLine("500 5.0.0 Error during DATA transmission")
break
}
if ddata == "." {
if strings.Contains(datastring, "DATA close should fail") {
_ = writeLine("500 5.0.0 Error during DATA closing")
break
}
_ = writeLine("250 2.0.0 Ok: queued as 1234567890")
break
}
datastring += ddata + "\n"
}
case strings.EqualFold(data, "noop"),
strings.EqualFold(data, "vrfy"):
writeOK()
case strings.EqualFold(data, "rset"):
if failReset {
_ = writeLine("500 5.1.2 Error: reset failed")
break
}
writeOK()
case strings.EqualFold(data, "quit"):
_ = writeLine("221 2.0.0 Bye")
default:
_ = writeLine("500 5.5.2 Error: bad syntax")
}
}
}

4
msg.go
View file

@ -482,8 +482,8 @@ func (m *Msg) SetMessageID() {
if err != nil { if err != nil {
hostname = "localhost.localdomain" hostname = "localhost.localdomain"
} }
randNumPrimary := randNum(100000000) randNumPrimary, _ := randNum(100000000)
randNumSecondary := randNum(10000) randNumSecondary, _ := randNum(10000)
randString, _ := randomStringSecure(17) randString, _ := randomStringSecure(17)
procID := os.Getpid() * randNumSecondary procID := os.Getpid() * randNumSecondary
messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary, messageID := fmt.Sprintf("%d.%d%d.%s@%s", procID, randNumPrimary, randNumSecondary,

View file

@ -61,25 +61,3 @@ func TestMsg_WriteToSendmail(t *testing.T) {
t.Errorf("WriteToSendmail failed: %s", err) t.Errorf("WriteToSendmail failed: %s", err)
} }
} }
func TestMsg_WriteToTempFileFailed(t *testing.T) {
m := NewMsg()
_ = m.From("Toni Tester <tester@example.com>")
_ = m.To("Ellenor Tester <ellinor@example.com>")
m.SetBodyString(TypeTextPlain, "This is a test")
curTmpDir := os.Getenv("TMPDIR")
defer func() {
if err := os.Setenv("TMPDIR", curTmpDir); err != nil {
t.Errorf("failed to set TMPDIR environment variable: %s", err)
}
}()
if err := os.Setenv("TMPDIR", "/invalid/directory/that/does/not/exist"); err != nil {
t.Errorf("failed to set TMPDIR environment variable: %s", err)
}
_, err := m.WriteToTempFile()
if err == nil {
t.Errorf("WriteToTempFile() did not fail as expected")
}
}

View file

@ -786,11 +786,13 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods // TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
func TestMsg_SetMessageIDRandomness(t *testing.T) { func TestMsg_SetMessageIDRandomness(t *testing.T) {
var mids []string var mids []string
for i := 0; i < 50_000; i++ { for i := 0; i < 100; i++ {
m := NewMsg() m := NewMsg()
m.SetMessageID() m.SetMessageID()
mid := m.GetMessageID() mid := m.GetGenHeader(HeaderMessageID)
mids = append(mids, mid) if len(mid) > 0 {
mids = append(mids, mid[0])
}
} }
c := make(map[string]int) c := make(map[string]int)
for i := range mids { for i := range mids {

View file

@ -7,6 +7,8 @@ package mail
import ( import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"fmt"
"math/big"
"strings" "strings"
) )
@ -50,3 +52,23 @@ func randomStringSecure(length int) (string, error) {
return randString.String(), nil return randString.String(), nil
} }
// randNum returns a random number with a maximum value of length
func randNum(length int) (int, error) {
if length <= 0 {
return 0, fmt.Errorf("provided number is <= 0: %d", length)
}
length64 := big.NewInt(int64(length))
if !length64.IsUint64() {
return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", length64)
}
randNum64, err := rand.Int(rand.Reader, length64)
if err != nil {
return 0, err
}
randomNum := int(randNum64.Int64())
if randomNum < 0 {
return 0, fmt.Errorf("generated random number does not fit as int64: %d", randNum64)
}
return randomNum, nil
}

View file

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build go1.19 && !go1.20
// +build go1.19,!go1.20
package mail
import (
"math/rand"
"time"
)
// randNum returns a random number with a maximum value of length
func randNum(maxval int) int {
if maxval <= 0 {
return 0
}
rand.Seed(time.Now().UnixNano())
return rand.Intn(maxval)
}

View file

@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build go1.20 && !go1.22
// +build go1.20,!go1.22
package mail
import (
"math/rand"
)
// randNum returns a random number with a maximum value of length
func randNum(maxval int) int {
if maxval <= 0 {
return 0
}
return rand.Intn(maxval)
}

View file

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
//go:build go1.22
// +build go1.22
package mail
import (
"math/rand/v2"
)
// randNum returns a random number with a maximum value of maxval.
// go-mail compiled with Go 1.22+ will make use of the novel math/rand/v2 interface
// Older versions of Go will use math/rand
func randNum(maxval int) int {
if maxval <= 0 {
return 0
}
return rand.IntN(maxval)
}

View file

@ -55,17 +55,16 @@ func TestRandomNum(t *testing.T) {
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
rn := randNum(tc.max) rn, err := randNum(tc.max)
if err != nil {
t.Errorf("random number generation failed: %s", err)
}
if rn < 0 {
t.Errorf("random number generation failed: %d is smaller than zero", rn)
}
if rn > tc.max { if rn > tc.max {
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max) t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
} }
}) })
} }
} }
func TestRandomNumZero(t *testing.T) {
rn := randNum(0)
if rn != 0 {
t.Errorf("random number generation failed: %d is not zero", rn)
}
}

View file

@ -118,23 +118,6 @@ func (e *SendError) IsTemp() bool {
return e.isTemp return e.isTemp
} }
// MessageID returns the message ID of the affected Msg that caused the error
// If no message ID was set for the Msg, an empty string will be returned
func (e *SendError) MessageID() string {
if e == nil || e.affectedMsg == nil {
return ""
}
return e.affectedMsg.GetMessageID()
}
// Msg returns the pointer to the affected message that caused the error
func (e *SendError) Msg() *Msg {
if e == nil || e.affectedMsg == nil {
return nil
}
return e.affectedMsg
}
// String implements the Stringer interface for the SendErrReason // String implements the Stringer interface for the SendErrReason
func (r SendErrReason) String() string { func (r SendErrReason) String() string {
switch r { switch r {

View file

@ -90,89 +90,7 @@ func TestSendError_IsTempNil(t *testing.T) {
} }
} }
func TestSendError_MessageID(t *testing.T) {
var se *SendError
err := returnSendError(ErrAmbiguous, false)
if !errors.As(err, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
}
if errors.As(err, &se) {
if se.MessageID() == "" {
t.Errorf("sendError expected message-id, but got empty string")
}
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
se.MessageID())
}
}
}
func TestSendError_MessageIDNil(t *testing.T) {
var se *SendError
if se.MessageID() != "" {
t.Error("expected empty string on nil-senderror")
}
}
func TestSendError_Msg(t *testing.T) {
var se *SendError
err := returnSendError(ErrAmbiguous, false)
if !errors.As(err, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
}
if errors.As(err, &se) {
if se.Msg() == nil {
t.Errorf("sendError expected msg pointer, but got nil")
}
from := se.Msg().GetFromString()
if len(from) == 0 {
t.Errorf("sendError expected msg from, but got empty string")
return
}
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
from[0])
}
}
}
func TestSendError_MsgNil(t *testing.T) {
var se *SendError
if se.Msg() != nil {
t.Error("expected nil on nil-senderror")
}
}
func TestSendError_IsFail(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, false)
err2 := returnSendError(ErrSMTPMailFrom, false)
if errors.Is(err1, err2) {
t.Errorf("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom")
}
}
func TestSendError_ErrorMulti(t *testing.T) {
expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` +
`affected recipient(s): <email1@domain.tld>, <email2@domain.tld>`
err := &SendError{
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
}
if err.Error() != expected {
t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error())
}
}
// returnSendError is a helper method to retunr a SendError with a specific reason // returnSendError is a helper method to retunr a SendError with a specific reason
func returnSendError(r SendErrReason, t bool) error { func returnSendError(r SendErrReason, t bool) error {
message := NewMsg() return &SendError{Reason: r, isTemp: t}
_ = message.From("toni.tester@domain.tld")
_ = message.To("tina.tester@domain.tld")
message.Subject("This is the subject")
message.SetBodyString(TypeTextPlain, "This is the message body")
message.SetMessageIDWithValue("this.is.a.message.id")
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
} }