From 27a39852408a219be409a82d8e07a631c14552fb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 15:24:47 +0100 Subject: [PATCH 001/125] Refactor and expand random string tests Refactored the test for `randomStringSecure` to better organize test cases using subtests. Added new test cases to check failures with a broken rand.Reader, improving test coverage and robustness. --- random_test.go | 86 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/random_test.go b/random_test.go index a608c2a..b59eba6 100644 --- a/random_test.go +++ b/random_test.go @@ -5,45 +5,81 @@ package mail import ( + "crypto/rand" + "errors" "strings" "testing" ) // TestRandomStringSecure tests the randomStringSecure method func TestRandomStringSecure(t *testing.T) { - tt := []struct { - testName string - length int - mustNotMatch string - }{ - {"20 chars", 20, "'"}, - {"100 chars", 100, "'"}, - {"1000 chars", 1000, "'"}, - } + t.Run("randomStringSecure with varying length", func(t *testing.T) { + tt := []struct { + testName string + length int + mustNotMatch string + }{ + {"20 chars", 20, "'"}, + {"100 chars", 100, "'"}, + {"1000 chars", 1000, "'"}, + } - for _, tc := range tt { - t.Run(tc.testName, func(t *testing.T) { - rs, err := randomStringSecure(tc.length) - if err != nil { - t.Errorf("random string generation failed: %s", err) - } - if strings.Contains(rs, tc.mustNotMatch) { - t.Errorf("random string contains unexpected character. got: %s, not-expected: %s", - rs, tc.mustNotMatch) - } - if len(rs) != tc.length { - t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs)) - } - }) - } + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + rs, err := randomStringSecure(tc.length) + if err != nil { + t.Errorf("random string generation failed: %s", err) + } + if strings.Contains(rs, tc.mustNotMatch) { + t.Errorf("random string contains unexpected character. got: %s, not-expected: %s", + rs, tc.mustNotMatch) + } + if len(rs) != tc.length { + t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs)) + } + }) + } + }) + t.Run("randomStringSecure fails on broken rand Reader (first read)", func(t *testing.T) { + defaultRandReader := rand.Reader + t.Cleanup(func() { rand.Reader = defaultRandReader }) + rand.Reader = &randReader{failon: 1} + if _, err := randomStringSecure(22); err == nil { + t.Fatalf("expected failure on broken rand Reader") + } + }) + t.Run("randomStringSecure fails on broken rand Reader (second read)", func(t *testing.T) { + defaultRandReader := rand.Reader + t.Cleanup(func() { rand.Reader = defaultRandReader }) + rand.Reader = &randReader{failon: 0} + if _, err := randomStringSecure(22); err == nil { + t.Fatalf("expected failure on broken rand Reader") + } + }) } func BenchmarkGenerator_RandomStringSecure(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - _, err := randomStringSecure(22) + _, err := randomStringSecure(10) if err != nil { b.Errorf("RandomStringFromCharRange() failed: %s", err) } } } + +// randReader is type that satisfies the io.Reader interface. It can fail on a specific read +// operations and is therefore useful to test consecutive reads with errors +type randReader struct { + failon uint8 + call uint8 +} + +// Read implements the io.Reader interface for the randReader type +func (r *randReader) Read(p []byte) (int, error) { + if r.call == r.failon { + r.call++ + return len(p), nil + } + return 0, errors.New("broken reader") +} From 25b7f81e3bbbef64a9d66bc08cfa43e58ec106ce Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 15:57:59 +0100 Subject: [PATCH 002/125] Refactor error handling logic and string formatting Replaced constant with named error for readability and maintainability in the error handling condition. Adjusted error message formatting by removing an extra space for consistency. --- senderror.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/senderror.go b/senderror.go index 1943e28..4471208 100644 --- a/senderror.go +++ b/senderror.go @@ -81,7 +81,7 @@ type SendErrReason int // Returns: // - A string representing the error message. func (e *SendError) Error() string { - if e.Reason > 10 { + if e.Reason > ErrAmbiguous { return "unknown reason" } @@ -93,7 +93,7 @@ func (e *SendError) Error() string { errMessage.WriteRune(' ') errMessage.WriteString(e.errlist[i].Error()) if i != len(e.errlist)-1 { - errMessage.WriteString(", ") + errMessage.WriteString(",") } } } From e37dd39654a90541486e0e1a520ade419220a6f3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 15:58:11 +0100 Subject: [PATCH 003/125] Refactor `senderror_test.go` for improved test clarity Consolidated multiple duplicate test cases into grouped sub-tests with clear names. This enhances readability and maintainability, ensures proper test isolation, and removes redundant code. --- senderror_test.go | 306 ++++++++++++++++++++++++++-------------------- 1 file changed, 176 insertions(+), 130 deletions(-) diff --git a/senderror_test.go b/senderror_test.go index e04b7ee..3cd3a76 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -6,163 +6,210 @@ package mail import ( "errors" - "fmt" "strings" "testing" ) // TestSendError_Error tests the SendError and SendErrReason error handling methods func TestSendError_Error(t *testing.T) { - tl := []struct { - n string - r SendErrReason - te bool - }{ - {"ErrGetSender/temp", ErrGetSender, true}, - {"ErrGetSender/perm", ErrGetSender, false}, - {"ErrGetRcpts/temp", ErrGetRcpts, true}, - {"ErrGetRcpts/perm", ErrGetRcpts, false}, - {"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true}, - {"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false}, - {"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true}, - {"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false}, - {"ErrSMTPData/temp", ErrSMTPData, true}, - {"ErrSMTPData/perm", ErrSMTPData, false}, - {"ErrSMTPDataClose/temp", ErrSMTPDataClose, true}, - {"ErrSMTPDataClose/perm", ErrSMTPDataClose, false}, - {"ErrSMTPReset/temp", ErrSMTPReset, true}, - {"ErrSMTPReset/perm", ErrSMTPReset, false}, - {"ErrWriteContent/temp", ErrWriteContent, true}, - {"ErrWriteContent/perm", ErrWriteContent, false}, - {"ErrConnCheck/temp", ErrConnCheck, true}, - {"ErrConnCheck/perm", ErrConnCheck, false}, - {"ErrNoUnencoded/temp", ErrNoUnencoded, true}, - {"ErrNoUnencoded/perm", ErrNoUnencoded, false}, - {"ErrAmbiguous/temp", ErrAmbiguous, true}, - {"ErrAmbiguous/perm", ErrAmbiguous, false}, - {"Unknown/temp", 9999, true}, - {"Unknown/perm", 9999, false}, - } - - for _, tt := range tl { - t.Run(tt.n, func(t *testing.T) { - if err := returnSendError(tt.r, tt.te); err != nil { - exp := &SendError{Reason: tt.r, isTemp: tt.te} - if !errors.Is(err, exp) { - t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te, - exp.Error(), exp.isTemp) + t.Run("TestSendError_Error with various reasons", func(t *testing.T) { + tests := []struct { + name string + reason SendErrReason + isTemp bool + }{ + {"ErrGetSender/temp", ErrGetSender, true}, + {"ErrGetSender/perm", ErrGetSender, false}, + {"ErrGetRcpts/temp", ErrGetRcpts, true}, + {"ErrGetRcpts/perm", ErrGetRcpts, false}, + {"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true}, + {"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false}, + {"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true}, + {"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false}, + {"ErrSMTPData/temp", ErrSMTPData, true}, + {"ErrSMTPData/perm", ErrSMTPData, false}, + {"ErrSMTPDataClose/temp", ErrSMTPDataClose, true}, + {"ErrSMTPDataClose/perm", ErrSMTPDataClose, false}, + {"ErrSMTPReset/temp", ErrSMTPReset, true}, + {"ErrSMTPReset/perm", ErrSMTPReset, false}, + {"ErrWriteContent/temp", ErrWriteContent, true}, + {"ErrWriteContent/perm", ErrWriteContent, false}, + {"ErrConnCheck/temp", ErrConnCheck, true}, + {"ErrConnCheck/perm", ErrConnCheck, false}, + {"ErrNoUnencoded/temp", ErrNoUnencoded, true}, + {"ErrNoUnencoded/perm", ErrNoUnencoded, false}, + {"ErrAmbiguous/temp", ErrAmbiguous, true}, + {"ErrAmbiguous/perm", ErrAmbiguous, false}, + {"Unknown/temp", 9999, true}, + {"Unknown/perm", 9999, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := returnSendError(tt.reason, tt.isTemp) + if err == nil { + t.Fatalf("error expected, got nil") } - if !strings.Contains(fmt.Sprintf("%s", err), tt.r.String()) { + want := &SendError{Reason: tt.reason, isTemp: tt.isTemp} + if !errors.Is(err, want) { + t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", + tt.reason, tt.isTemp, want.Error(), want.isTemp) + } + if !strings.Contains(err.Error(), tt.reason.String()) { t.Errorf("error string mismatch, expected: %s, got: %s", - tt.r.String(), fmt.Sprintf("%s", err)) + tt.reason.String(), err.Error()) } - } - }) - } + }) + } + }) + t.Run("TestSendError_Error with multiple errors", func(t *testing.T) { + message := testMessage(t) + err := &SendError{ + affectedMsg: message, + errlist: []error{ErrNoRcptAddresses, ErrNoFromAddress}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + } + if !strings.Contains(err.Error(), "ambiguous reason, check Msg.SendError for message specific reasons") { + t.Errorf("error string mismatch, expected: ambiguous reason, check Msg.SendError for message "+ + "specific reasons, got: %s", err.Error()) + } + if !strings.Contains(err.Error(), "no recipient addresses set, no FROM address set") { + t.Errorf("error string mismatch, expected: no recipient addresses set, no FROM address set, got: %s", + err.Error()) + } + if !strings.Contains(err.Error(), "affected recipient(s): , "+ + "") { + t.Errorf("error string mismatch, expected: affected recipient(s): , "+ + ", got: %s", err.Error()) + } + }) +} + +func TestSendError_Is(t *testing.T) { + t.Run("TestSendError_Is errors match", func(t *testing.T) { + err1 := returnSendError(ErrAmbiguous, false) + err2 := returnSendError(ErrAmbiguous, false) + if !errors.Is(err1, err2) { + t.Error("error mismatch, expected ErrAmbiguous to be equal to ErrAmbiguous") + } + }) + t.Run("TestSendError_Is errors mismatch", func(t *testing.T) { + err1 := returnSendError(ErrAmbiguous, false) + err2 := returnSendError(ErrSMTPMailFrom, false) + if errors.Is(err1, err2) { + t.Error("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom") + } + }) + t.Run("TestSendError_Is on nil", func(t *testing.T) { + var err *SendError + if err.Is(ErrNoFromAddress) { + t.Error("expected false on nil-senderror") + } + }) } func TestSendError_IsTemp(t *testing.T) { - var se *SendError - err1 := returnSendError(ErrAmbiguous, true) - if !errors.As(err1, &se) { - t.Errorf("error mismatch, expected error to be of type *SendError") - return - } - if errors.As(err1, &se) && !se.IsTemp() { - t.Errorf("error mismatch, expected temporary error") - return - } - err2 := returnSendError(ErrAmbiguous, false) - if !errors.As(err2, &se) { - t.Errorf("error mismatch, expected error to be of type *SendError") - return - } - if errors.As(err2, &se) && se.IsTemp() { - t.Errorf("error mismatch, expected non-temporary error") - return - } -} - -func TestSendError_IsTempNil(t *testing.T) { - var se *SendError - if se.IsTemp() { - t.Error("expected false on nil-senderror") - } + t.Run("TestSendError_IsTemp is true", func(t *testing.T) { + err := returnSendError(ErrAmbiguous, true) + if err == nil { + t.Fatalf("error expected, got nil") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Fatal("error expected to be of type *SendError") + } + if !sendErr.IsTemp() { + t.Errorf("expected temporary error, got: temperr: %t", sendErr.IsTemp()) + } + }) + t.Run("TestSendError_IsTemp is false", func(t *testing.T) { + err := returnSendError(ErrAmbiguous, false) + if err == nil { + t.Fatalf("error expected, got nil") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Fatal("error expected to be of type *SendError") + } + if sendErr.IsTemp() { + t.Errorf("expected permanent error, got: temperr: %t", sendErr.IsTemp()) + } + }) + t.Run("TestSendError_IsTemp is nil", func(t *testing.T) { + var se *SendError + if se.IsTemp() { + t.Error("expected false on nil-senderror") + } + }) } 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") + t.Run("TestSendError_MessageID message ID is set", func(t *testing.T) { + var sendErr *SendError + err := returnSendError(ErrAmbiguous, false) + if !errors.As(err, &sendErr) { + t.Fatal("error mismatch, expected error to be of type *SendError") } - if !strings.EqualFold(se.MessageID(), "") { + if sendErr.MessageID() == "" { + t.Error("sendError expected message-id, but got empty string") + } + if !strings.EqualFold(sendErr.MessageID(), "") { t.Errorf("sendError message-id expected: %s, but got: %s", "", - se.MessageID()) + sendErr.MessageID()) } - } -} - -func TestSendError_MessageIDNil(t *testing.T) { - var se *SendError - if se.MessageID() != "" { - t.Error("expected empty string on nil-senderror") - } + }) + t.Run("TestSendError_MessageID message ID is not set", func(t *testing.T) { + var sendErr *SendError + message := testMessage(t) + err := &SendError{ + affectedMsg: message, + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + } + if !errors.As(err, &sendErr) { + t.Fatal("error mismatch, expected error to be of type *SendError") + } + if sendErr.MessageID() != "" { + t.Errorf("sendError expected empty message-id, got: %s", sendErr.MessageID()) + } + }) } 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") + t.Run("TestSendError_Msg message is set", func(t *testing.T) { + var sendErr *SendError + err := returnSendError(ErrAmbiguous, false) + if !errors.As(err, &sendErr) { + t.Fatal("error mismatch, expected error to be of type *SendError") } - from := se.Msg().GetFromString() + msg := sendErr.Msg() + if msg == nil { + t.Fatalf("sendError expected msg pointer, but got nil") + } + from := msg.GetFromString() if len(from) == 0 { - t.Errorf("sendError expected msg from, but got empty string") - return + t.Fatal("sendError expected msg from, but got empty string") } if !strings.EqualFold(from[0], "") { t.Errorf("sendError message from expected: %s, but got: %s", "", 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): , ` - err := &SendError{ - Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil, - rcpt: []string{"", ""}, - } - if err.Error() != expected { - t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error()) - } + }) + t.Run("TestSendError_Msg message is not set", func(t *testing.T) { + var sendErr *SendError + err := &SendError{ + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + } + if !errors.As(err, &sendErr) { + t.Fatal("error mismatch, expected error to be of type *SendError") + } + if sendErr.Msg() != nil { + t.Errorf("sendError expected nil msg pointer, got: %v", sendErr.Msg()) + } + }) } // returnSendError is a helper method to retunr a SendError with a specific reason @@ -173,6 +220,5 @@ func returnSendError(r SendErrReason, t bool) error { 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} } From 0fcde10768d2cd1d0fbd6af3cc970a7956b60c21 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 16:33:48 +0100 Subject: [PATCH 004/125] Remove output redirection from sendmail install This change ensures that the output of the apt-get commands is no longer redirected to /dev/null. This aids in debugging by making command outputs visible in the CI logs. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37e0363..f0411d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: check-latest: true - name: Install sendmail run: | - sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail + sudo apt-get -y update && sudo apt-get -y upgrade && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer && which sendmail - name: Run go test if: success() run: | From ec10e0b13289191b1dc7decc33bcc8e9c473b19b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 16:36:06 +0100 Subject: [PATCH 005/125] Remove redundant upgrade command in CI workflow The `sudo apt-get -y upgrade` command was removed from the CI workflow's "Install sendmail" step. This change simplifies the installation process by ensuring only the necessary updates and installations are performed, which can contribute to faster and more reliable CI runs. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0411d3..2901602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: check-latest: true - name: Install sendmail run: | - sudo apt-get -y update && sudo apt-get -y upgrade && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer && which sendmail + sudo apt-get -y update && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer && which sendmail - name: Run go test if: success() run: | From a3fe2f88d58b7107bf88bbd1ca05086fb4369bdd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 18:47:21 +0100 Subject: [PATCH 006/125] Add test for MessageID on nil SendError This test ensures that when MessageID is called on a nil SendError, it returns an empty string. This additional check helps verify the correct behavior of the MessageID method under nil conditions. --- senderror_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/senderror_test.go b/senderror_test.go index 3cd3a76..8584e32 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -174,6 +174,12 @@ func TestSendError_MessageID(t *testing.T) { t.Errorf("sendError expected empty message-id, got: %s", sendErr.MessageID()) } }) + t.Run("TestSendError_MessageID on nil error should return empty", func(t *testing.T) { + var sendErr *SendError + if sendErr.MessageID() != "" { + t.Error("expected empty message-id on nil-senderror") + } + }) } func TestSendError_Msg(t *testing.T) { From 99c4378107d42a6fb9ae9e41446b24d14bee1088 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 1 Nov 2024 19:22:28 +0100 Subject: [PATCH 007/125] Refactor and streamline authentication tests Improved the structure and readability of the authentication tests by using subtests for each scenario, ensuring better isolation and clearer failure reporting. Removed unnecessary imports and redundant code, reducing complexity and enhancing maintainability. --- smtp/smtp_test.go | 206 ++++++++++++++++++++++++++++------------------ 1 file changed, 125 insertions(+), 81 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 4fe0481..a7f3236 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -14,29 +14,9 @@ package smtp import ( - "bufio" "bytes" - "crypto/hmac" - "crypto/sha1" - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "encoding/base64" - "flag" - "fmt" - "hash" - "io" - "net" - "net/textproto" - "os" - "runtime" - "strings" + "errors" "testing" - "time" - - "golang.org/x/crypto/pbkdf2" - - "github.com/wneessen/go-mail/log" ) type authTest struct { @@ -180,91 +160,152 @@ var authTests = []authTest{ } func TestAuth(t *testing.T) { -testLoop: - for i, test := range authTests { - name, resp, err := test.auth.Start(&ServerInfo{"testserver", true, nil}) - if name != test.name { - t.Errorf("#%d got name %s, expected %s", i, name, test.name) - } - if !bytes.Equal(resp, []byte(test.responses[0])) { - t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0]) - } - if err != nil { - t.Errorf("#%d error: %s", i, err) - } - for j := range test.challenges { - challenge := []byte(test.challenges[j]) - expected := []byte(test.responses[j+1]) - sf := test.sf[j] - resp, err := test.auth.Next(challenge, true) - if err != nil && !sf { - t.Errorf("#%d error: %s", i, err) - continue testLoop - } - if test.hasNonce { - if !bytes.HasPrefix(resp, expected) { - t.Errorf("#%d got response: %s, expected response to start with: %s", i, resp, expected) + t.Run("Auth for all supported auth methods", func(t *testing.T) { + for i, tt := range authTests { + t.Run(tt.name, func(t *testing.T) { + name, resp, err := tt.auth.Start(&ServerInfo{"testserver", true, nil}) + if name != tt.name { + t.Errorf("test #%d got name %s, expected %s", i, name, tt.name) } - continue testLoop - } - if !bytes.Equal(resp, expected) { - t.Errorf("#%d got %s, expected %s", i, resp, expected) - continue testLoop - } - _, err = test.auth.Next([]byte("2.7.0 Authentication successful"), false) - if err != nil { - t.Errorf("#%d success message error: %s", i, err) - } + if len(tt.responses) <= 0 { + t.Fatalf("test #%d got no responses, expected at least one", i) + } + if !bytes.Equal(resp, []byte(tt.responses[0])) { + t.Errorf("#%d got response %s, expected %s", i, resp, tt.responses[0]) + } + if err != nil { + t.Errorf("#%d error: %s", i, err) + } + testLoop: + for j := range tt.challenges { + challenge := []byte(tt.challenges[j]) + expected := []byte(tt.responses[j+1]) + sf := tt.sf[j] + resp, err := tt.auth.Next(challenge, true) + if err != nil && !sf { + t.Errorf("#%d error: %s", i, err) + continue testLoop + } + if tt.hasNonce { + if !bytes.HasPrefix(resp, expected) { + t.Errorf("#%d got response: %s, expected response to start with: %s", i, resp, expected) + } + continue testLoop + } + if !bytes.Equal(resp, expected) { + t.Errorf("#%d got %s, expected %s", i, resp, expected) + continue testLoop + } + _, err = tt.auth.Next([]byte("2.7.0 Authentication successful"), false) + if err != nil { + t.Errorf("#%d success message error: %s", i, err) + } + } + }) } - } + }) } -func TestAuthPlain(t *testing.T) { +func TestPlainAuth(t *testing.T) { tests := []struct { - authName string - server *ServerInfo - err string + name string + authName string + server *ServerInfo + shouldFail bool + wantErr error }{ { - authName: "servername", - server: &ServerInfo{Name: "servername", TLS: true}, + name: "PLAIN auth succeeds", + authName: "servername", + server: &ServerInfo{Name: "servername", TLS: true}, + shouldFail: false, }, { // OK to use PlainAuth on localhost without TLS - authName: "localhost", - server: &ServerInfo{Name: "localhost", TLS: false}, + name: "PLAIN on localhost is allowed to go unencrypted", + authName: "localhost", + server: &ServerInfo{Name: "localhost", TLS: false}, + shouldFail: false, }, { // NOT OK on non-localhost, even if server says PLAIN is OK. // (We don't know that the server is the real server.) - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, - err: "unencrypted connection", + name: "PLAIN on non-localhost is not allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, + shouldFail: true, + wantErr: ErrUnencrypted, }, { - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, - err: "unencrypted connection", + name: "PLAIN on non-localhost with no PLAIN announcement, is not allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, + shouldFail: true, + wantErr: ErrUnencrypted, }, { - authName: "servername", - server: &ServerInfo{Name: "attacker", TLS: true}, - err: "wrong host name", + name: "PLAIN with wrong hostname", + authName: "servername", + server: &ServerInfo{Name: "attacker", TLS: true}, + shouldFail: true, + wantErr: ErrWrongHostname, }, } - for i, tt := range tests { - auth := PlainAuth("foo", "bar", "baz", tt.authName, false) - _, _, err := auth.Start(tt.server) - got := "" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + identity := "foo" + user := "toni.tester@example.com" + pass := "v3ryS3Cur3P4ssw0rd" + auth := PlainAuth(identity, user, pass, tt.authName, false) + method, resp, err := auth.Start(tt.server) + if err != nil && !tt.shouldFail { + t.Errorf("plain authentication failed: %s", err) + } + if err == nil && tt.shouldFail { + t.Error("plain authentication was expected to fail") + } + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Errorf("expected error to be: %s, got: %s", tt.wantErr, err) + } + return + } + if method != "PLAIN" { + t.Errorf("expected method return to be: %q, got: %q", "PLAIN", method) + } + if !bytes.Equal([]byte(identity+"\x00"+user+"\x00"+pass), resp) { + t.Errorf("expected response to be: %q, got: %q", identity+"\x00"+user+"\x00"+pass, resp) + } + }) + } + t.Run("PLAIN sends second server response should fail", func(t *testing.T) { + identity := "foo" + user := "toni.tester@example.com" + pass := "v3ryS3Cur3P4ssw0rd" + server := &ServerInfo{Name: "servername", TLS: true} + auth := PlainAuth(identity, user, pass, "servername", false) + method, resp, err := auth.Start(server) if err != nil { - got = err.Error() + t.Fatalf("plain authentication failed: %s", err) } - if got != tt.err { - t.Errorf("%d. got error = %q; want %q", i, got, tt.err) + if method != "PLAIN" { + t.Errorf("expected method return to be: %q, got: %q", "PLAIN", method) } - } + if !bytes.Equal([]byte(identity+"\x00"+user+"\x00"+pass), resp) { + t.Errorf("expected response to be: %q, got: %q", identity+"\x00"+user+"\x00"+pass, resp) + } + _, err = auth.Next([]byte("nonsense"), true) + if err == nil { + t.Fatal("expected second server challange to fail") + } + if !errors.Is(err, ErrUnexpectedServerChallange) { + t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerChallange, err) + } + }) } +/* + func TestAuthPlainNoEnc(t *testing.T) { tests := []struct { authName string @@ -2555,3 +2596,6 @@ func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash) go server.handleConnection(conn) } } + + +*/ From 3cfd20576d9d37bee26db07868f2b442a12eafbc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 3 Nov 2024 16:13:54 +0100 Subject: [PATCH 008/125] Rename and expand `TestPlainAuth_noEnc` with additional checks Refactor the test function `TestPlainAuth_noEnc` to include subtests for better organization and add more comprehensive error handling. This improves clarity and robustness by verifying various authentication scenarios and expected outcomes. --- smtp/smtp_test.go | 106 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 25 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index a7f3236..3022018 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -304,48 +304,104 @@ func TestPlainAuth(t *testing.T) { }) } -/* - -func TestAuthPlainNoEnc(t *testing.T) { +func TestPlainAuth_noEnc(t *testing.T) { tests := []struct { - authName string - server *ServerInfo - err string + name string + authName string + server *ServerInfo + shouldFail bool + wantErr error }{ { - authName: "servername", - server: &ServerInfo{Name: "servername", TLS: true}, + name: "PLAIN-NOENC auth succeeds", + authName: "servername", + server: &ServerInfo{Name: "servername", TLS: true}, + shouldFail: false, }, { // OK to use PlainAuth on localhost without TLS - authName: "localhost", - server: &ServerInfo{Name: "localhost", TLS: false}, + name: "PLAIN-NOENC on localhost is allowed to go unencrypted", + authName: "localhost", + server: &ServerInfo{Name: "localhost", TLS: false}, + shouldFail: false, }, { - // Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow - // non-encrypted connections. - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, + // ALSO OK on non-localhost. This auth mode is specificly for that. + name: "PLAIN-NOENC on non-localhost is allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}}, + shouldFail: false, }, { - authName: "servername", - server: &ServerInfo{Name: "attacker", TLS: true}, - err: "wrong host name", + name: "PLAIN-NOENC on non-localhost with no PLAIN announcement, is allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, + shouldFail: false, + }, + { + name: "PLAIN-NOENC with wrong hostname", + authName: "servername", + server: &ServerInfo{Name: "attacker", TLS: true}, + shouldFail: true, + wantErr: ErrWrongHostname, }, } - for i, tt := range tests { - auth := PlainAuth("foo", "bar", "baz", tt.authName, true) - _, _, err := auth.Start(tt.server) - got := "" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + identity := "foo" + user := "toni.tester@example.com" + pass := "v3ryS3Cur3P4ssw0rd" + auth := PlainAuth(identity, user, pass, tt.authName, true) + method, resp, err := auth.Start(tt.server) + if err != nil && !tt.shouldFail { + t.Errorf("plain authentication failed: %s", err) + } + if err == nil && tt.shouldFail { + t.Error("plain authentication was expected to fail") + } + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Errorf("expected error to be: %s, got: %s", tt.wantErr, err) + } + return + } + if method != "PLAIN" { + t.Errorf("expected method return to be: %q, got: %q", "PLAIN", method) + } + if !bytes.Equal([]byte(identity+"\x00"+user+"\x00"+pass), resp) { + t.Errorf("expected response to be: %q, got: %q", identity+"\x00"+user+"\x00"+pass, resp) + } + }) + } + t.Run("PLAIN-NOENC sends second server response should fail", func(t *testing.T) { + identity := "foo" + user := "toni.tester@example.com" + pass := "v3ryS3Cur3P4ssw0rd" + server := &ServerInfo{Name: "servername", TLS: true} + auth := PlainAuth(identity, user, pass, "servername", true) + method, resp, err := auth.Start(server) if err != nil { - got = err.Error() + t.Fatalf("plain authentication failed: %s", err) } - if got != tt.err { - t.Errorf("%d. got error = %q; want %q", i, got, tt.err) + if method != "PLAIN" { + t.Errorf("expected method return to be: %q, got: %q", "PLAIN", method) } - } + if !bytes.Equal([]byte(identity+"\x00"+user+"\x00"+pass), resp) { + t.Errorf("expected response to be: %q, got: %q", identity+"\x00"+user+"\x00"+pass, resp) + } + _, err = auth.Next([]byte("nonsense"), true) + if err == nil { + t.Fatal("expected second server challange to fail") + } + if !errors.Is(err, ErrUnexpectedServerChallange) { + t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerChallange, err) + } + }) } +/* + + func TestAuthLogin(t *testing.T) { tests := []struct { authName string From 8c4eb62360af707726f414ce9ea824e1dbda9de9 Mon Sep 17 00:00:00 2001 From: Alysson Ribeiro <15274059+sonalys@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:20:59 +0100 Subject: [PATCH 009/125] Fix(close): Access to nil variable causes panic --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index abb90f4..fbb28f5 100644 --- a/client.go +++ b/client.go @@ -996,7 +996,7 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { // Returns: // - An error if the disconnection fails; otherwise, returns nil. func (c *Client) Close() error { - if !c.smtpClient.HasConnection() { + if c.smtpClient == nil || !c.smtpClient.HasConnection() { return nil } if err := c.smtpClient.Quit(); err != nil { From a5fcb3ae8bd9ea47e932093bfa6ac629a9166412 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 6 Nov 2024 10:47:31 +0100 Subject: [PATCH 010/125] Add test for closing a nil smtpclient Introduce a unit test to ensure that invoking Close on a nil smtpclient instance returns nil without errors. This enhances the robustness of the client closure functionality. This test accommodates the fix provided with PR #353 --- client_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client_test.go b/client_test.go index 664f0fd..96ebf1f 100644 --- a/client_test.go +++ b/client_test.go @@ -1647,6 +1647,15 @@ func TestClient_Close(t *testing.T) { t.Errorf("close was supposed to fail, but didn't") } }) + t.Run("close on a nil smtpclient should return nil", func(t *testing.T) { + client, err := NewClient(DefaultHost) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close the client: %s", err) + } + }) } func TestClient_DialWithContext(t *testing.T) { From b4aa414a4dd11049205849d6122e7397f3d357dd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 6 Nov 2024 11:22:54 +0100 Subject: [PATCH 011/125] Update doc.go Bump version to v0.5.2 --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 88a12ee..6878d86 100644 --- a/doc.go +++ b/doc.go @@ -11,4 +11,4 @@ package mail // VERSION indicates the current version of the package. It is also attached to the default user // agent string. -const VERSION = "0.5.1" +const VERSION = "0.5.2" From 2391010e3a77aef60666408e9eadba7675dc2758 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Nov 2024 20:58:20 +0100 Subject: [PATCH 012/125] Rename parameter for consistency in auth functions Updated the parameter name `allowUnEnc` to `allowUnenc` in both `LoginAuth` and `PlainAuth` functions to maintain consistent naming conventions. This change improves code readability and follows standard naming practices. --- smtp/auth_login.go | 4 ++-- smtp/auth_plain.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/smtp/auth_login.go b/smtp/auth_login.go index b5f1065..c40e48c 100644 --- a/smtp/auth_login.go +++ b/smtp/auth_login.go @@ -36,8 +36,8 @@ type loginAuth struct { // LoginAuth will only send the credentials if the connection is using TLS // or is connected to localhost. Otherwise authentication will fail with an // error, without sending the credentials. -func LoginAuth(username, password, host string, allowUnEnc bool) Auth { - return &loginAuth{username, password, host, 0, allowUnEnc} +func LoginAuth(username, password, host string, allowUnenc bool) Auth { + return &loginAuth{username, password, host, 0, allowUnenc} } // Start begins the SMTP authentication process by validating server's TLS status and hostname. diff --git a/smtp/auth_plain.go b/smtp/auth_plain.go index f2ea8ac..39e2a2f 100644 --- a/smtp/auth_plain.go +++ b/smtp/auth_plain.go @@ -28,8 +28,8 @@ type plainAuth struct { // PlainAuth will only send the credentials if the connection is using TLS // or is connected to localhost. Otherwise authentication will fail with an // error, without sending the credentials. -func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth { - return &plainAuth{identity, username, password, host, allowUnEnc} +func PlainAuth(identity, username, password, host string, allowUnenc bool) Auth { + return &plainAuth{identity, username, password, host, allowUnenc} } func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { From 410343496c138daab115fac155d8f0d6b74a10ad Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Nov 2024 21:14:52 +0100 Subject: [PATCH 013/125] Refactor and expand TestLoginAuth Rename and uncomment TestLoginAuth with more test cases, ensuring coverage for successful and failed authentication scenarios, including checks for unencrypted logins and server response errors. This improves test robustness and coverage. --- smtp/smtp_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 3022018..271ea5c 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -399,6 +399,97 @@ func TestPlainAuth_noEnc(t *testing.T) { }) } +func TestLoginAuth(t *testing.T) { + tests := []struct { + name string + authName string + server *ServerInfo + shouldFail bool + wantErr error + }{ + { + name: "LOGIN auth succeeds", + authName: "servername", + server: &ServerInfo{Name: "servername", TLS: true}, + shouldFail: false, + }, + { + // OK to use PlainAuth on localhost without TLS + name: "LOGIN on localhost is allowed to go unencrypted", + authName: "localhost", + server: &ServerInfo{Name: "localhost", TLS: false}, + shouldFail: false, + }, + { + // NOT OK on non-localhost, even if server says LOGIN is OK. + // (We don't know that the server is the real server.) + name: "LOGIN on non-localhost is not allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}}, + shouldFail: true, + wantErr: ErrUnencrypted, + }, + { + name: "LOGIN on non-localhost with no LOGIN announcement, is not allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, + shouldFail: true, + wantErr: ErrUnencrypted, + }, + { + name: "LOGIN with wrong hostname", + authName: "servername", + server: &ServerInfo{Name: "attacker", TLS: true}, + shouldFail: true, + wantErr: ErrWrongHostname, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user := "toni.tester@example.com" + pass := "v3ryS3Cur3P4ssw0rd" + auth := LoginAuth(user, pass, tt.authName, false) + method, _, err := auth.Start(tt.server) + if err != nil && !tt.shouldFail { + t.Errorf("plain authentication failed: %s", err) + } + if err == nil && tt.shouldFail { + t.Error("plain authentication was expected to fail") + } + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Errorf("expected error to be: %s, got: %s", tt.wantErr, err) + } + return + } + if method != "LOGIN" { + t.Errorf("expected method return to be: %q, got: %q", "LOGIN", method) + } + resp, err := auth.Next([]byte(user), true) + if err != nil { + t.Errorf("failed on first server challange: %s", err) + } + if !bytes.Equal([]byte(user), resp) { + t.Errorf("expected response to first challange to be: %q, got: %q", user, resp) + } + resp, err = auth.Next([]byte(pass), true) + if err != nil { + t.Errorf("failed on second server challange: %s", err) + } + if !bytes.Equal([]byte(pass), resp) { + t.Errorf("expected response to second challange to be: %q, got: %q", pass, resp) + } + resp, err = auth.Next([]byte("nonsense"), true) + if err == nil { + t.Error("expected third server challange to fail, but didn't") + } + if !errors.Is(err, ErrUnexpectedServerResponse) { + t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerResponse, err) + } + }) + } +} + /* @@ -408,10 +499,6 @@ func TestAuthLogin(t *testing.T) { server *ServerInfo err string }{ - { - authName: "servername", - server: &ServerInfo{Name: "servername", TLS: true}, - }, { // OK to use LoginAuth on localhost without TLS authName: "localhost", From 4221d48644d85d0fd624875d2b08cf04c6b29fe4 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Nov 2024 21:31:24 +0100 Subject: [PATCH 014/125] Update login authentication test cases in smtp_test.go Renamed the test functions and improved the test structure for login authentication checks. Added subtests to provide clear descriptions and enhance error checking. --- smtp/smtp_test.go | 176 ++++++++++++++++++++++++---------------------- 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 271ea5c..be578fb 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -451,10 +451,98 @@ func TestLoginAuth(t *testing.T) { auth := LoginAuth(user, pass, tt.authName, false) method, _, err := auth.Start(tt.server) if err != nil && !tt.shouldFail { - t.Errorf("plain authentication failed: %s", err) + t.Errorf("login authentication failed: %s", err) } if err == nil && tt.shouldFail { - t.Error("plain authentication was expected to fail") + t.Error("login authentication was expected to fail") + } + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Errorf("expected error to be: %s, got: %s", tt.wantErr, err) + } + return + } + if method != "LOGIN" { + t.Errorf("expected method return to be: %q, got: %q", "LOGIN", method) + } + resp, err := auth.Next([]byte(user), true) + if err != nil { + t.Errorf("failed on first server challange: %s", err) + } + if !bytes.Equal([]byte(user), resp) { + t.Errorf("expected response to first challange to be: %q, got: %q", user, resp) + } + resp, err = auth.Next([]byte(pass), true) + if err != nil { + t.Errorf("failed on second server challange: %s", err) + } + if !bytes.Equal([]byte(pass), resp) { + t.Errorf("expected response to second challange to be: %q, got: %q", pass, resp) + } + resp, err = auth.Next([]byte("nonsense"), true) + if err == nil { + t.Error("expected third server challange to fail, but didn't") + } + if !errors.Is(err, ErrUnexpectedServerResponse) { + t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerResponse, err) + } + }) + } +} + +func TestLoginAuth_noEnc(t *testing.T) { + tests := []struct { + name string + authName string + server *ServerInfo + shouldFail bool + wantErr error + }{ + { + name: "LOGIN-NOENC auth succeeds", + authName: "servername", + server: &ServerInfo{Name: "servername", TLS: true}, + shouldFail: false, + }, + { + // OK to use PlainAuth on localhost without TLS + name: "LOGIN-NOENC on localhost is allowed to go unencrypted", + authName: "localhost", + server: &ServerInfo{Name: "localhost", TLS: false}, + shouldFail: false, + }, + { + // ALSO OK on non-localhost. This auth mode is specificly for that. + name: "LOGIN-NOENC on non-localhost is allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}}, + shouldFail: false, + }, + { + name: "LOGIN-NOENC on non-localhost with no LOGIN announcement, is not allowed to go unencrypted", + authName: "servername", + server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, + shouldFail: false, + }, + { + name: "LOGIN-NOENC with wrong hostname", + authName: "servername", + server: &ServerInfo{Name: "attacker", TLS: true}, + shouldFail: true, + wantErr: ErrWrongHostname, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + user := "toni.tester@example.com" + pass := "v3ryS3Cur3P4ssw0rd" + auth := LoginAuth(user, pass, tt.authName, true) + method, _, err := auth.Start(tt.server) + if err != nil && !tt.shouldFail { + t.Errorf("login authentication failed: %s", err) + } + if err == nil && tt.shouldFail { + t.Error("login authentication was expected to fail") } if tt.wantErr != nil { if !errors.Is(err, tt.wantErr) { @@ -493,91 +581,7 @@ func TestLoginAuth(t *testing.T) { /* -func TestAuthLogin(t *testing.T) { - tests := []struct { - authName string - server *ServerInfo - err string - }{ - { - // OK to use LoginAuth on localhost without TLS - authName: "localhost", - server: &ServerInfo{Name: "localhost", TLS: false}, - }, - { - // NOT OK on non-localhost, even if server says PLAIN is OK. - // (We don't know that the server is the real server.) - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}}, - err: "unencrypted connection", - }, - { - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, - err: "unencrypted connection", - }, - { - authName: "servername", - server: &ServerInfo{Name: "attacker", TLS: true}, - err: "wrong host name", - }, - } - for i, tt := range tests { - auth := LoginAuth("foo", "bar", tt.authName, false) - _, _, err := auth.Start(tt.server) - got := "" - if err != nil { - got = err.Error() - } - if got != tt.err { - t.Errorf("%d. got error = %q; want %q", i, got, tt.err) - } - } -} -func TestAuthLoginNoEnc(t *testing.T) { - tests := []struct { - authName string - server *ServerInfo - err string - }{ - { - authName: "servername", - server: &ServerInfo{Name: "servername", TLS: true}, - }, - { - // OK to use LoginAuth on localhost without TLS - authName: "localhost", - server: &ServerInfo{Name: "localhost", TLS: false}, - }, - { - // Also OK on non-TLS secured connections. The NoEnc mechanism is meant to allow - // non-encrypted connections. - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"LOGIN"}}, - }, - { - authName: "servername", - server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}}, - }, - { - authName: "servername", - server: &ServerInfo{Name: "attacker", TLS: true}, - err: "wrong host name", - }, - } - for i, tt := range tests { - auth := LoginAuth("foo", "bar", tt.authName, true) - _, _, err := auth.Start(tt.server) - got := "" - if err != nil { - got = err.Error() - } - if got != tt.err { - t.Errorf("%d. got error = %q; want %q", i, got, tt.err) - } - } -} func TestXOAuth2OK(t *testing.T) { server := []string{ From b03fbb4ae8675dcf7ffd12730918efd079189674 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 7 Nov 2024 22:42:23 +0100 Subject: [PATCH 015/125] Add test server for SMTP authentication Added a simple SMTP test server with basic features like PLAIN, LOGIN, and NOENC authentication. It can start, handle connections, and simulate authentication success or failure. Included support for TLS with a generated localhost certificate. --- smtp/smtp_test.go | 496 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index be578fb..efc209d 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -14,11 +14,69 @@ package smtp import ( + "bufio" "bytes" + "context" + "crypto/tls" "errors" + "fmt" + "net" + "strings" + "sync/atomic" "testing" + "time" ) +const ( + // TestServerProto is the protocol used for the simple SMTP test server + 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 = 12025 +) + +// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. +var PortAdder atomic.Int32 + +// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: +// +// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ +// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var localhostCert = []byte(` +-----BEGIN CERTIFICATE----- +MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 +MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa +JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30 +LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF +MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA +AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi +NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4 +n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF +tN8URjVmyEo= +-----END CERTIFICATE-----`) + +// localhostKey is the private key for localhostCert. +var localhostKey = []byte(testingKey(` +-----BEGIN RSA TESTING KEY----- +MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h +AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu +lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB +AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA +kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM +VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m +542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb +PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2 +6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB +vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP +QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i +jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c +qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg== +-----END RSA TESTING KEY-----`)) + type authTest struct { auth Auth challenges []string @@ -302,6 +360,59 @@ func TestPlainAuth(t *testing.T) { t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerChallange, err) } }) + t.Run("PLAIN authentication on test server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := PlainAuth("", "user", "pass", TestServerAddr, false) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to test server: %s", err) + } + }) + t.Run("PLAIN authentication on test server should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := PlainAuth("", "user", "pass", TestServerAddr, false) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err == nil { + t.Errorf("expected authentication to fail") + } + }) } func TestPlainAuth_noEnc(t *testing.T) { @@ -397,6 +508,59 @@ func TestPlainAuth_noEnc(t *testing.T) { t.Errorf("expected error to be: %s, got: %s", ErrUnexpectedServerChallange, err) } }) + t.Run("PLAIN-NOENC authentication on test server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := PlainAuth("", "user", "pass", TestServerAddr, true) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to test server: %s", err) + } + }) + t.Run("PLAIN-NOENC authentication on test server should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := PlainAuth("", "user", "pass", TestServerAddr, true) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err == nil { + t.Errorf("expected authentication to fail") + } + }) } func TestLoginAuth(t *testing.T) { @@ -488,6 +652,59 @@ func TestLoginAuth(t *testing.T) { } }) } + t.Run("LOGIN authentication on test server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := LoginAuth("user", "pass", TestServerAddr, false) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to test server: %s", err) + } + }) + t.Run("LOGIN authentication on test server should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := LoginAuth("user", "pass", TestServerAddr, false) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err == nil { + t.Errorf("expected authentication to fail") + } + }) } func TestLoginAuth_noEnc(t *testing.T) { @@ -576,6 +793,59 @@ func TestLoginAuth_noEnc(t *testing.T) { } }) } + t.Run("LOGIN-NOENC authentication on test server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := LoginAuth("user", "pass", TestServerAddr, true) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to test server: %s", err) + } + }) + t.Run("LOGIN-NOENC authentication on test server should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := LoginAuth("user", "pass", TestServerAddr, true) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err == nil { + t.Errorf("expected authentication to fail") + } + }) } /* @@ -2746,3 +3016,229 @@ func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash) */ +// testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s. +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } + +// serverProps represents the configuration properties for the SMTP server. +type serverProps struct { + FailOnAuth bool + FailOnDataInit bool + FailOnDataClose bool + FailOnHelo bool + FailOnMailFrom bool + FailOnNoop bool + FailOnQuit bool + FailOnReset bool + FailOnSTARTTLS bool + FailTemp bool + FeatureSet string + ListenPort int + SSLListener bool + IsTLS bool + SupportDSN bool +} + +// 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, t *testing.T, props *serverProps) error { + t.Helper() + if props == nil { + return fmt.Errorf("no server properties provided") + } + + var listener net.Listener + var err error + if props.SSLListener { + keypair, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + return fmt.Errorf("failed to read TLS keypair: %w", err) + } + tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} + listener, err = tls.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort), + tlsConfig) + if err != nil { + t.Fatalf("failed to create TLS listener: %s", err) + } + } else { + listener, err = net.Listen(TestServerProto, fmt.Sprintf("%s:%d", TestServerAddr, props.ListenPort)) + } + if err != nil { + return fmt.Errorf("unable to listen on %s://%s: %w (SSL: %t)", TestServerProto, TestServerAddr, err, + props.SSLListener) + } + + defer func() { + if err := listener.Close(); err != nil { + t.Logf("failed to close listener: %s", err) + } + }() + + 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, t, props) + } + } +} + +func handleTestServerConnection(connection net.Conn, t *testing.T, props *serverProps) { + t.Helper() + if !props.IsTLS { + t.Cleanup(func() { + if err := connection.Close(); err != nil { + t.Logf("failed to close connection: %s", err) + } + }) + } + + reader := bufio.NewReader(connection) + writer := bufio.NewWriter(connection) + + writeLine := func(data string) { + _, err := writer.WriteString(data + "\r\n") + if err != nil { + t.Logf("failed to write line: %s", err) + } + _ = writer.Flush() + } + writeOK := func() { + writeLine("250 2.0.0 OK") + } + + if !props.IsTLS { + writeLine("220 go-mail test server ready ESMTP") + } + + for { + data, err := reader.ReadString('\n') + if err != nil { + break + } + time.Sleep(time.Millisecond) + + var datastring string + data = strings.TrimSpace(data) + switch { + case strings.HasPrefix(data, "EHLO"), strings.HasPrefix(data, "HELO"): + if len(strings.Split(data, " ")) != 2 { + writeLine("501 Syntax: EHLO hostname") + break + } + if props.FailOnHelo { + writeLine("500 5.5.2 Error: fail on HELO") + break + } + writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + case strings.HasPrefix(data, "MAIL FROM:"): + if props.FailOnMailFrom { + writeLine("500 5.5.2 Error: fail on MAIL FROM") + break + } + from := strings.TrimPrefix(data, "MAIL FROM:") + from = strings.ReplaceAll(from, "BODY=8BITMIME", "") + from = strings.ReplaceAll(from, "SMTPUTF8", "") + if props.SupportDSN { + from = strings.ReplaceAll(from, "RET=FULL", "") + } + from = strings.TrimSpace(from) + if !strings.EqualFold(from, "") { + 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:") + if props.SupportDSN { + to = strings.ReplaceAll(to, "NOTIFY=FAILURE,SUCCESS", "") + } + to = strings.TrimSpace(to) + if !strings.EqualFold(to, "") { + writeLine(fmt.Sprintf("500 5.1.2 Invalid to: %s", to)) + break + } + writeOK() + case strings.HasPrefix(data, "AUTH"): + if props.FailOnAuth { + writeLine("535 5.7.8 Error: authentication failed") + break + } + writeLine("235 2.7.0 Authentication successful") + case strings.EqualFold(data, "DATA"): + if props.FailOnDataInit { + writeLine("503 5.5.1 Error: fail on DATA init") + break + } + writeLine("354 End data with .") + for { + ddata, derr := reader.ReadString('\n') + if derr != nil { + t.Logf("failed to read data from connection: %s", derr) + break + } + ddata = strings.TrimSpace(ddata) + if ddata == "." { + if props.FailOnDataClose { + writeLine("500 5.0.0 Error during DATA transmission") + break + } + if props.FailTemp { + writeLine("451 4.3.0 Error: fail on DATA close") + break + } + writeLine("250 2.0.0 Ok: queued as 1234567890") + break + } + datastring += ddata + "\n" + } + case strings.EqualFold(data, "noop"): + if props.FailOnNoop { + writeLine("500 5.0.0 Error: fail on NOOP") + break + } + writeOK() + case strings.EqualFold(data, "vrfy"): + writeOK() + case strings.EqualFold(data, "rset"): + if props.FailOnReset { + writeLine("500 5.1.2 Error: reset failed") + break + } + writeOK() + case strings.EqualFold(data, "quit"): + if props.FailOnQuit { + writeLine("500 5.1.2 Error: quit failed") + break + } + writeLine("221 2.0.0 Bye") + return + case strings.EqualFold(data, "starttls"): + if props.FailOnSTARTTLS { + writeLine("500 5.1.2 Error: starttls failed") + break + } + keypair, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + writeLine("500 5.1.2 Error: starttls failed - " + err.Error()) + break + } + writeLine("220 Ready to start TLS") + tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} + connection = tls.Server(connection, tlsConfig) + props.IsTLS = true + handleTestServerConnection(connection, t, props) + default: + writeLine("500 5.5.2 Error: bad syntax") + } + } +} From 62ea3f56af0aba9b6b887013bc55baf5aa7b5a46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:31:55 +0000 Subject: [PATCH 016/125] Bump golang.org/x/crypto from 0.28.0 to 0.29.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.28.0 to 0.29.0. - [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.29.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 4fb64a8..72ce847 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,6 @@ module github.com/wneessen/go-mail go 1.16 require ( - golang.org/x/crypto v0.28.0 - golang.org/x/text v0.19.0 + golang.org/x/crypto v0.29.0 + golang.org/x/text v0.20.0 ) diff --git a/go.sum b/go.sum index 8e6bffc..30d3dc0 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -26,7 +26,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -37,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -46,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -55,8 +55,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From a3ef47ac938124b28b3df78975e35d990fdcdcd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:05:00 +0000 Subject: [PATCH 017/125] Bump sonarsource/sonarqube-scan-action from 3.0.0 to 3.1.0 Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/884b79409bbd464b2a59edc326a4b77dc56b2195...13990a695682794b53148ff9f6a8b6e22e43955e) --- updated-dependencies: - dependency-name: sonarsource/sonarqube-scan-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2901602..2a842e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -208,7 +208,7 @@ jobs: run: | go test -shuffle=on -race --coverprofile=./cov.out ./... - name: SonarQube scan - uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master + uses: sonarsource/sonarqube-scan-action@13990a695682794b53148ff9f6a8b6e22e43955e # master if: success() env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 1af17f14e12b3501226a69392a7df31cb64bcce6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Nov 2024 15:11:47 +0100 Subject: [PATCH 018/125] Add cleanup to close client connections in tests This commit enhances the cleanup process in the SMTP tests by adding t.Cleanup to close client connections. Additionally, it rewrites the TestXOAuth2 function to include more detailed sub-tests, enhancing test granularity and readability. --- smtp/smtp_test.go | 242 +++++++++++++++++++++++++++------------------- 1 file changed, 145 insertions(+), 97 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index efc209d..cf3760c 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "errors" "fmt" + "io" "net" "strings" "sync/atomic" @@ -382,6 +383,11 @@ func TestPlainAuth(t *testing.T) { if err != nil { t.Fatalf("failed to connect to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) if err = client.Auth(auth); err != nil { t.Errorf("failed to authenticate to test server: %s", err) } @@ -530,6 +536,11 @@ func TestPlainAuth_noEnc(t *testing.T) { if err != nil { t.Fatalf("failed to connect to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) if err = client.Auth(auth); err != nil { t.Errorf("failed to authenticate to test server: %s", err) } @@ -674,6 +685,11 @@ func TestLoginAuth(t *testing.T) { if err != nil { t.Fatalf("failed to connect to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) if err = client.Auth(auth); err != nil { t.Errorf("failed to authenticate to test server: %s", err) } @@ -815,6 +831,11 @@ func TestLoginAuth_noEnc(t *testing.T) { if err != nil { t.Fatalf("failed to connect to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) if err = client.Auth(auth); err != nil { t.Errorf("failed to authenticate to test server: %s", err) } @@ -848,98 +869,123 @@ func TestLoginAuth_noEnc(t *testing.T) { }) } +func TestXOAuth2Auth(t *testing.T) { + t.Run("XOAuth2 authentication all steps", func(t *testing.T) { + auth := XOAuth2Auth("user", "token") + proto, toserver, err := auth.Start(&ServerInfo{Name: "servername", TLS: true}) + if err != nil { + t.Fatalf("failed to start XOAuth2 authentication: %s", err) + } + if proto != "XOAUTH2" { + t.Errorf("expected protocol to be XOAUTH2, got: %q", proto) + } + expected := []byte("user=user\x01auth=Bearer token\x01\x01") + if !bytes.Equal(expected, toserver) { + t.Errorf("expected server response to be: %q, got: %q", expected, toserver) + } + resp, err := auth.Next([]byte("nonsense"), true) + if err != nil { + t.Errorf("failed on first server challange: %s", err) + } + if !bytes.Equal([]byte(""), resp) { + t.Errorf("expected server response to be empty, got: %q", resp) + } + resp, err = auth.Next([]byte("nonsense"), false) + if err != nil { + t.Errorf("failed on first server challange: %s", err) + } + }) + t.Run("XOAuth2 succeeds with faker", func(t *testing.T) { + server := []string{ + "220 Fake server ready ESMTP", + "250-fake.server", + "250-AUTH XOAUTH2", + "250 8BITMIME", + "235 2.7.0 Accepted", + } + var wrote strings.Builder + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(strings.Join(server, "\r\n")), + &wrote, + } + client, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("failed to create client on faker server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + + auth := XOAuth2Auth("user", "token") + if err = client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to faker server: %s", err) + } + + // the Next method returns a nil response. It must not be sent. + // The client request must end with the authentication. + if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") { + t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String()) + } + }) + t.Run("XOAuth2 fails with faker", func(t *testing.T) { + serverResp := []string{ + "220 Fake server ready ESMTP", + "250-fake.server", + "250-AUTH XOAUTH2", + "250 8BITMIME", + "334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==", + "535 5.7.8 Username and Password not accepted", + "221 2.0.0 closing connection", + } + var wrote strings.Builder + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(strings.Join(serverResp, "\r\n")), + &wrote, + } + client, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("failed to create client on faker server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + + auth := XOAuth2Auth("user", "token") + if err = client.Auth(auth); err == nil { + t.Errorf("expected authentication to fail") + } + resp := strings.Split(wrote.String(), "\r\n") + if len(resp) != 5 { + t.Fatalf("unexpected number of client requests got %d; want 5", len(resp)) + } + if resp[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" { + t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", resp[1]) + } + // the Next method returns an empty response. It must be sent + if resp[2] != "" { + t.Fatalf("got %q; want empty response", resp[2]) + } + }) +} + /* -func TestXOAuth2OK(t *testing.T) { - server := []string{ - "220 Fake server ready ESMTP", - "250-fake.server", - "250-AUTH XOAUTH2", - "250 8BITMIME", - "235 2.7.0 Accepted", - } - var wrote strings.Builder - var fake faker - fake.ReadWriter = struct { - io.Reader - io.Writer - }{ - strings.NewReader(strings.Join(server, "\r\n")), - &wrote, - } - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v", err) - } - defer func() { - if err := c.Close(); err != nil { - t.Errorf("failed to close client: %s", err) - } - }() - - auth := XOAuth2Auth("user", "token") - err = c.Auth(auth) - if err != nil { - t.Fatalf("XOAuth2 error: %v", err) - } - // the Next method returns a nil response. It must not be sent. - // The client request must end with the authentication. - if !strings.HasSuffix(wrote.String(), "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n") { - t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", wrote.String()) - } -} - -func TestXOAuth2Error(t *testing.T) { - serverResp := []string{ - "220 Fake server ready ESMTP", - "250-fake.server", - "250-AUTH XOAUTH2", - "250 8BITMIME", - "334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==", - "535 5.7.8 Username and Password not accepted", - "221 2.0.0 closing connection", - } - var wrote strings.Builder - var fake faker - fake.ReadWriter = struct { - io.Reader - io.Writer - }{ - strings.NewReader(strings.Join(serverResp, "\r\n")), - &wrote, - } - - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v", err) - } - defer func() { - if err := c.Close(); err != nil { - t.Errorf("failed to close client: %s", err) - } - }() - - auth := XOAuth2Auth("user", "token") - err = c.Auth(auth) - if err == nil { - t.Fatal("expected auth error, got nil") - } - client := strings.Split(wrote.String(), "\r\n") - if len(client) != 5 { - t.Fatalf("unexpected number of client requests got %d; want 5", len(client)) - } - if client[1] != "AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=" { - t.Fatalf("got %q; want AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIHRva2VuAQE=", client[1]) - } - // the Next method returns an empty response. It must be sent - if client[2] != "" { - t.Fatalf("got %q; want empty response", client[2]) - } -} func TestAuthSCRAMSHA1_OK(t *testing.T) { hostname := "127.0.0.1" @@ -1217,17 +1263,6 @@ func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) { panic("unexpected call") } -type faker struct { - io.ReadWriter -} - -func (f faker) Close() error { return nil } -func (f faker) LocalAddr() net.Addr { return nil } -func (f faker) RemoteAddr() net.Addr { return nil } -func (f faker) SetDeadline(time.Time) error { return nil } -func (f faker) SetReadDeadline(time.Time) error { return nil } -func (f faker) SetWriteDeadline(time.Time) error { return nil } - func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") @@ -3016,6 +3051,19 @@ func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash) */ + +// faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. +type faker struct { + io.ReadWriter +} + +func (f faker) Close() error { return nil } +func (f faker) LocalAddr() net.Addr { return nil } +func (f faker) RemoteAddr() net.Addr { return nil } +func (f faker) SetDeadline(time.Time) error { return nil } +func (f faker) SetReadDeadline(time.Time) error { return nil } +func (f faker) SetWriteDeadline(time.Time) error { return nil } + // testingKey replaces the substring "TESTING KEY" with "PRIVATE KEY" in the given string s. func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } From c656226fd3a0b786dbe4fe859f42215882952efd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Nov 2024 15:51:17 +0100 Subject: [PATCH 019/125] Add XOAuth2 authentication tests to SMTP package Introduces two tests for XOAuth2 authentication in the SMTP package. The first test ensures successful authentication with valid credentials, while the second test verifies that authentication fails with incorrect settings. --- smtp/smtp_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index cf3760c..5846e7f 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -978,6 +978,64 @@ func TestXOAuth2Auth(t *testing.T) { t.Fatalf("got %q; want empty response", resp[2]) } }) + t.Run("XOAuth2 authentication on test server succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH XOAUTH2\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := XOAuth2Auth("user", "token") + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + if err = client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to test server: %s", err) + } + }) + t.Run("XOAuth2 authentication on test server fails", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH XOAUTH2\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort}, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + auth := XOAuth2Auth("user", "token") + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + if err = client.Auth(auth); err == nil { + t.Errorf("expected authentication to fail") + } + }) } /* From d4c6cb506c8ef8fc3828b16e107e5535a3fe860b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Nov 2024 16:53:09 +0100 Subject: [PATCH 020/125] Add SCRAM authentication tests to smtp package Added comprehensive unit tests for SCRAM-SHA-1, SCRAM-SHA-256, and their PLUS variants. Implemented a test server to simulate various SCRAM authentication scenarios and validate both success and failure cases. --- smtp/smtp_test.go | 352 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 5846e7f..70f8d04 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -17,15 +17,22 @@ import ( "bufio" "bytes" "context" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" "crypto/tls" + "encoding/base64" "errors" "fmt" + "hash" "io" "net" "strings" "sync/atomic" "testing" "time" + + "golang.org/x/crypto/pbkdf2" ) const ( @@ -1038,6 +1045,178 @@ func TestXOAuth2Auth(t *testing.T) { }) } +func TestScramAuth(t *testing.T) { + tests := []struct { + name string + tls bool + authString string + hash func() hash.Hash + isPlus bool + }{ + {"SCRAM-SHA-1 (no TLS)", false, "SCRAM-SHA-1", sha1.New, false}, + {"SCRAM-SHA-256 (no TLS)", false, "SCRAM-SHA-256", sha256.New, false}, + {"SCRAM-SHA-1 (with TLS)", true, "SCRAM-SHA-1", sha1.New, false}, + {"SCRAM-SHA-256 (with TLS)", true, "SCRAM-SHA-256", sha256.New, false}, + {"SCRAM-SHA-1-PLUS", true, "SCRAM-SHA-1-PLUS", sha1.New, true}, + {"SCRAM-SHA-256-PLUS", true, "SCRAM-SHA-256-PLUS", sha256.New, true}, + } + for _, tt := range tests { + t.Run(tt.name+" succeeds on test server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := fmt.Sprintf("250-AUTH %s\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8", tt.authString) + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + TestSCRAM: true, + HashFunc: tt.hash, + FeatureSet: featureSet, + ListenPort: serverPort, + SSLListener: tt.tls, + IsSCRAMPlus: tt.isPlus, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + var client *Client + switch tt.tls { + case true: + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), &tlsConfig) + if err != nil { + t.Fatalf("failed to dial TLS server: %v", err) + } + client, err = NewClient(conn, TestServerAddr) + case false: + var err error + client, err = Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client connection: %s", err) + } + }) + + var auth Auth + switch tt.authString { + case "SCRAM-SHA-1": + auth = ScramSHA1Auth("username", "password") + case "SCRAM-SHA-256": + auth = ScramSHA256Auth("username", "password") + case "SCRAM-SHA-1-PLUS": + tlsConnState, err := client.GetTLSConnectionState() + if err != nil { + t.Fatalf("failed to get TLS connection state: %s", err) + } + auth = ScramSHA1PlusAuth("username", "password", tlsConnState) + case "SCRAM-SHA-256-PLUS": + tlsConnState, err := client.GetTLSConnectionState() + if err != nil { + t.Fatalf("failed to get TLS connection state: %s", err) + } + auth = ScramSHA256PlusAuth("username", "password", tlsConnState) + default: + t.Fatalf("unexpected auth string: %s", tt.authString) + } + if err := client.Auth(auth); err != nil { + t.Errorf("failed to authenticate to test server: %s", err) + } + }) + t.Run(tt.name+" fails on test server", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := fmt.Sprintf("250-AUTH %s\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8", tt.authString) + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + TestSCRAM: true, + HashFunc: tt.hash, + FeatureSet: featureSet, + ListenPort: serverPort, + SSLListener: tt.tls, + IsSCRAMPlus: tt.isPlus, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + var client *Client + switch tt.tls { + case true: + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + fmt.Printf("error creating TLS cert: %s", err) + return + } + tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), &tlsConfig) + if err != nil { + t.Fatalf("failed to dial TLS server: %v", err) + } + client, err = NewClient(conn, TestServerAddr) + case false: + var err error + client, err = Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } + } + + var auth Auth + switch tt.authString { + case "SCRAM-SHA-1": + auth = ScramSHA1Auth("invalid", "password") + case "SCRAM-SHA-256": + auth = ScramSHA256Auth("invalid", "password") + case "SCRAM-SHA-1-PLUS": + tlsConnState, err := client.GetTLSConnectionState() + if err != nil { + t.Fatalf("failed to get TLS connection state: %s", err) + } + auth = ScramSHA1PlusAuth("invalid", "password", tlsConnState) + case "SCRAM-SHA-256-PLUS": + tlsConnState, err := client.GetTLSConnectionState() + if err != nil { + t.Fatalf("failed to get TLS connection state: %s", err) + } + auth = ScramSHA256PlusAuth("invalid", "password", tlsConnState) + default: + t.Fatalf("unexpected auth string: %s", tt.authString) + } + if err := client.Auth(auth); err == nil { + t.Error("expected authentication to fail") + } + }) + } + t.Run("ScramAuth_Next with nonsense parameter", func(t *testing.T) { + auth := ScramSHA1Auth("username", "password") + _, err := auth.Next([]byte("x=nonsense"), true) + if err == nil { + t.Fatal("expected authentication to fail") + } + if !errors.Is(err, ErrUnexpectedServerResponse) { + t.Errorf("expected ErrUnexpectedServerResponse, got %s", err) + } + }) +} + /* @@ -3140,8 +3319,11 @@ type serverProps struct { FeatureSet string ListenPort int SSLListener bool + IsSCRAMPlus bool IsTLS bool SupportDSN bool + TestSCRAM bool + HashFunc func() hash.Hash } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -3279,6 +3461,22 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server writeLine("535 5.7.8 Error: authentication failed") break } + if props.TestSCRAM { + parts := strings.Split(data, " ") + authMechanism := parts[1] + if authMechanism != "SCRAM-SHA-1" && authMechanism != "SCRAM-SHA-256" && + authMechanism != "SCRAM-SHA-1-PLUS" && authMechanism != "SCRAM-SHA-256-PLUS" { + writeLine("504 Unrecognized authentication mechanism") + break + } + scram := &testSCRAMSMTP{ + tlsServer: props.IsSCRAMPlus, + h: props.HashFunc, + } + writeLine("334 ") + scram.handleSCRAMAuth(connection) + break + } writeLine("235 2.7.0 Authentication successful") case strings.EqualFold(data, "DATA"): if props.FailOnDataInit { @@ -3348,3 +3546,157 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } } } + +// testSCRAMSMTP represents a part of the test server for SCRAM-based SMTP authentication. +// It does not do any acutal computation of the challenges but verifies that the expected +// fields are present. We have actual real authentication tests for all SCRAM modes in the +// go-mail client_test.go +type testSCRAMSMTP struct { + authMechanism string + nonce string + h func() hash.Hash + tlsServer bool +} + +func (s *testSCRAMSMTP) handleSCRAMAuth(conn net.Conn) { + reader := bufio.NewReader(conn) + writer := bufio.NewWriter(conn) + 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() + } + var authMsg string + + data, err := reader.ReadString('\n') + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + data = strings.TrimSpace(data) + decodedMessage, err := base64.StdEncoding.DecodeString(data) + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + splits := strings.Split(string(decodedMessage), ",") + if len(splits) != 4 { + _ = writeLine("535 Authentication failed - expected 4 parts") + return + } + if !s.tlsServer && splits[0] != "n" { + _ = writeLine("535 Authentication failed - expected n to be in the first part") + return + } + if s.tlsServer && !strings.HasPrefix(splits[0], "p=") { + _ = writeLine("535 Authentication failed - expected p= to be in the first part") + return + } + if splits[2] != "n=username" { + _ = writeLine("535 Authentication failed - expected n=username to be in the third part") + return + } + if !strings.HasPrefix(splits[3], "r=") { + _ = writeLine("535 Authentication failed - expected r= to be in the fourth part") + return + } + authMsg = splits[2] + "," + splits[3] + + clientNonce := s.extractNonce(string(decodedMessage)) + if clientNonce == "" { + _ = writeLine("535 Authentication failed") + return + } + + s.nonce = clientNonce + "server_nonce" + serverFirstMessage := fmt.Sprintf("r=%s,s=%s,i=4096", s.nonce, + base64.StdEncoding.EncodeToString([]byte("salt"))) + _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFirstMessage)))) + authMsg = authMsg + "," + serverFirstMessage + + data, err = reader.ReadString('\n') + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + data = strings.TrimSpace(data) + decodedFinalMessage, err := base64.StdEncoding.DecodeString(data) + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + splits = strings.Split(string(decodedFinalMessage), ",") + + if !s.tlsServer && splits[0] != "c=biws" { + _ = writeLine("535 Authentication failed - expected c=biws to be in the first part") + return + } + if s.tlsServer { + if !strings.HasPrefix(splits[0], "c=") { + _ = writeLine("535 Authentication failed - expected c= to be in the first part") + return + } + channelBind, err := base64.StdEncoding.DecodeString(splits[0][2:]) + if err != nil { + _ = writeLine("535 Authentication failed - base64 channel bind is not valid - " + err.Error()) + return + } + if !strings.HasPrefix(string(channelBind), "p=") { + _ = writeLine("535 Authentication failed - expected channel binding to start with p=-") + return + } + cbType := string(channelBind[2:]) + if !strings.HasPrefix(cbType, "tls-unique") && !strings.HasPrefix(cbType, "tls-exporter") { + _ = writeLine("535 Authentication failed - expected channel binding type tls-unique or tls-exporter") + return + } + } + + if !strings.HasPrefix(splits[1], "r=") { + _ = writeLine("535 Authentication failed - expected r to be in the second part") + return + } + if !strings.Contains(splits[1], "server_nonce") { + _ = writeLine("535 Authentication failed - expected server_nonce to be in the second part") + return + } + if !strings.HasPrefix(splits[2], "p=") { + _ = writeLine("535 Authentication failed - expected p to be in the third part") + return + } + + authMsg = authMsg + "," + splits[0] + "," + splits[1] + saltedPwd := pbkdf2.Key([]byte("password"), []byte("salt"), 4096, s.h().Size(), s.h) + mac := hmac.New(s.h, saltedPwd) + mac.Write([]byte("Server Key")) + skey := mac.Sum(nil) + mac.Reset() + + mac = hmac.New(s.h, skey) + mac.Write([]byte(authMsg)) + ssig := mac.Sum(nil) + mac.Reset() + + serverFinalMessage := fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(ssig)) + _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFinalMessage)))) + + _, err = reader.ReadString('\n') + if err != nil { + _ = writeLine("535 Authentication failed") + return + } + + _ = writeLine("235 Authentication successful") +} + +func (s *testSCRAMSMTP) extractNonce(message string) string { + parts := strings.Split(message, ",") + for _, part := range parts { + if strings.HasPrefix(part, "r=") { + return part[2:] + } + } + return "" +} From d6f256c29ed9e11a0adb89ed2509cb4927ffb673 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Nov 2024 22:30:46 +0100 Subject: [PATCH 021/125] Fix typo in error message in normalizeString function Corrected the spelling of "failed" in the error handling branch of the normalizeString function within smtp/auth_scram.go. This change addresses a minor typographical error to ensure the error message is clear and accurate. --- smtp/auth_scram.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index a21aef5..e27831b 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -308,7 +308,7 @@ func (a *scramAuth) normalizeUsername() (string, error) { func (a *scramAuth) normalizeString(s string) (string, error) { s, err := precis.OpaqueString.String(s) if err != nil { - return "", fmt.Errorf("failled to normalize string: %w", err) + return "", fmt.Errorf("failed to normalize string: %w", err) } if s == "" { return "", errors.New("normalized string is empty") From a1efa1a1caf81e78cda51b2a85ab377dcd5a812f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Nov 2024 22:44:10 +0100 Subject: [PATCH 022/125] Remove redundant empty string check in SCRAM normalization The existing check for an empty normalized string is unnecessary because the OpaqueString profile in precis already throws an error if an empty string is returned: https://cs.opensource.google/go/x/text/+/master:secure/precis/profiles.go;l=66 --- smtp/auth_scram.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index e27831b..23915ae 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -310,8 +310,5 @@ func (a *scramAuth) normalizeString(s string) (string, error) { if err != nil { return "", fmt.Errorf("failed to normalize string: %w", err) } - if s == "" { - return "", errors.New("normalized string is empty") - } return s, nil } From c6d416f142e73c2ade50b467f48b4c99a883ad03 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 8 Nov 2024 23:05:31 +0100 Subject: [PATCH 023/125] Fix typo in the tls-unique channel binding comment Corrected "crypto/tl" to "crypto/tls" in the comment for better clarity and accuracy. This typo fix ensures that the code comments correctly reference the relevant Go package. --- smtp/auth_scram.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/auth_scram.go b/smtp/auth_scram.go index 23915ae..03de54c 100644 --- a/smtp/auth_scram.go +++ b/smtp/auth_scram.go @@ -154,7 +154,7 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) { connState := a.tlsConnState bindData := connState.TLSUnique - // crypto/tl: no tls-unique channel binding value for this tls connection, possibly due to missing + // crypto/tls: no tls-unique channel binding value for this tls connection, possibly due to missing // extended master key support and/or resumed connection // RFC9266:122 tls-unique not defined for tls 1.3 and later if bindData == nil || connState.Version >= tls.VersionTLS13 { From 92ab51b13d28ad9266b9d7ecf55ceddf77cda49f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 9 Nov 2024 00:06:33 +0100 Subject: [PATCH 024/125] Add comprehensive tests for scramAuth error handling Introduce new test cases to validate the error handling of the scramAuth methods. These tests cover scenarios such as invalid characters in usernames, empty inputs, and edge cases like broken rand.Reader. --- smtp/smtp_test.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 70f8d04..238176a 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "crypto/hmac" + "crypto/rand" "crypto/sha1" "crypto/sha256" "crypto/tls" @@ -1217,6 +1218,182 @@ func TestScramAuth(t *testing.T) { }) } +func TestScramAuth_normalizeString(t *testing.T) { + t.Run("normalizeString with invalid input should fail", func(t *testing.T) { + auth := scramAuth{} + value := "\u0000example\uFFFEstring\u001F" + _, err := auth.normalizeString(value) + if err == nil { + t.Fatal("normalizeString should fail on disallowed runes") + } + if !strings.Contains(err.Error(), "precis: disallowed rune encountered") { + t.Errorf("expected error to be %q, got %q", "precis: disallowed rune encountered", err) + } + }) + t.Run("normalizeString on empty string should fail", func(t *testing.T) { + auth := scramAuth{} + _, err := auth.normalizeString("") + if err == nil { + t.Error("normalizeString should fail on disallowed runes") + } + if !strings.Contains(err.Error(), "precis: transformation resulted in empty string") { + t.Errorf("expected error to be %q, got %q", "precis: transformation resulted in empty string", err) + } + }) + t.Run("normalizeUsername with invalid input should fail", func(t *testing.T) { + auth := scramAuth{username: "\u0000example\uFFFEstring\u001F"} + _, err := auth.normalizeUsername() + if err == nil { + t.Error("normalizeUsername should fail on disallowed runes") + } + if !strings.Contains(err.Error(), "precis: disallowed rune encountered") { + t.Errorf("expected error to be %q, got %q", "precis: disallowed rune encountered", err) + } + }) + t.Run("normalizeUsername with empty input should fail", func(t *testing.T) { + auth := scramAuth{username: ""} + _, err := auth.normalizeUsername() + if err == nil { + t.Error("normalizeUsername should fail on empty input") + } + if !strings.Contains(err.Error(), "precis: transformation resulted in empty string") { + t.Errorf("expected error to be %q, got %q", "precis: transformation resulted in empty string", err) + } + }) +} + +func TestScramAuth_initialClientMessage(t *testing.T) { + t.Run("initialClientMessage with invalid username should fail", func(t *testing.T) { + auth := scramAuth{username: "\u0000example\uFFFEstring\u001F"} + _, err := auth.initialClientMessage() + if err == nil { + t.Error("initialClientMessage should fail on disallowed runes") + } + if !strings.Contains(err.Error(), "precis: disallowed rune encountered") { + t.Errorf("expected error to be %q, got %q", "precis: disallowed rune encountered", err) + } + }) + t.Run("initialClientMessage with empty username should fail", func(t *testing.T) { + auth := scramAuth{username: ""} + _, err := auth.initialClientMessage() + if err == nil { + t.Error("initialClientMessage should fail on empty username") + } + if !strings.Contains(err.Error(), "precis: transformation resulted in empty string") { + t.Errorf("expected error to be %q, got %q", "precis: transformation resulted in empty string", err) + } + }) + t.Run("initialClientMessage fails on broken rand.Reader", func(t *testing.T) { + defaultRandReader := rand.Reader + t.Cleanup(func() { rand.Reader = defaultRandReader }) + rand.Reader = &randReader{} + auth := scramAuth{username: "username"} + _, err := auth.initialClientMessage() + if err == nil { + t.Error("initialClientMessage should fail with broken rand.Reader") + } + if !strings.Contains(err.Error(), "unable to generate client secret: broken reader") { + t.Errorf("expected error to be %q, got %q", "unable to generate client secret: broken reader", err) + } + }) +} + +func TestScramAuth_handleServerFirstResponse(t *testing.T) { + t.Run("handleServerFirstResponse fails if not at least 3 parts", func(t *testing.T) { + auth := scramAuth{} + _, err := auth.handleServerFirstResponse([]byte("r=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := "not enough fields in the first server response" + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with first part does not start with r=", func(t *testing.T) { + auth := scramAuth{} + _, err := auth.handleServerFirstResponse([]byte("x=0,y=0,z=0,r=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := "first part of the server response does not start with r=" + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with second part does not start with s=", func(t *testing.T) { + auth := scramAuth{} + _, err := auth.handleServerFirstResponse([]byte("r=0,x=0,y=0,z=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := "second part of the server response does not start with s=" + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with third part does not start with i=", func(t *testing.T) { + auth := scramAuth{} + _, err := auth.handleServerFirstResponse([]byte("r=0,s=0,y=0,z=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := "third part of the server response does not start with i=" + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with empty nonce", func(t *testing.T) { + auth := scramAuth{} + _, err := auth.handleServerFirstResponse([]byte("r=,s=0,i=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := "server nonce does not start with our nonce" + if !strings.EqualFold(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with non-base64 nonce", func(t *testing.T) { + auth := scramAuth{nonce: []byte("Test123")} + _, err := auth.handleServerFirstResponse([]byte("r=Test123,s=0,i=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := "illegal base64 data at input byte 0" + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with non-number iterations", func(t *testing.T) { + auth := scramAuth{nonce: []byte("VGVzdDEyMw==")} + _, err := auth.handleServerFirstResponse([]byte("r=VGVzdDEyMw==,s=VGVzdDEyMw==,i=abc")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := `invalid iterations: strconv.Atoi: parsing "abc": invalid syntax` + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + t.Run("handleServerFirstResponse fails with invalid password runes", func(t *testing.T) { + auth := scramAuth{ + nonce: []byte("VGVzdDEyMw=="), + username: "username", + password: "\u0000example\uFFFEstring\u001F", + } + _, err := auth.handleServerFirstResponse([]byte("r=VGVzdDEyMw==,s=VGVzdDEyMw==,i=0")) + if err == nil { + t.Error("handleServerFirstResponse should fail on invalid response") + } + expectedErr := `unable to normalize password: failed to normalize string: precis: disallowed rune encountered` + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("expected error to be %q, got %q", expectedErr, err) + } + }) + +} + /* @@ -3700,3 +3877,12 @@ func (s *testSCRAMSMTP) extractNonce(message string) string { } return "" } + +// randReader is type that satisfies the io.Reader interface. It can fail on a specific read +// operations and is therefore useful to test consecutive reads with errors +type randReader struct{} + +// Read implements the io.Reader interface for the randReader type +func (r *randReader) Read([]byte) (int, error) { + return 0, errors.New("broken reader") +} From cefaa0d4eeb6fff849fba7771ce126c535981e04 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 9 Nov 2024 13:44:14 +0100 Subject: [PATCH 025/125] Refactor SMTP test cases for improved clarity and coverage Consolidate and organize SMTP test cases by removing obsolete tests and adding focused, detailed tests for CRAM-MD5 and new client behaviors. This ensures clearer test structure and better coverage of edge cases. --- smtp/smtp_test.go | 769 ++++++++++++++-------------------------------- 1 file changed, 229 insertions(+), 540 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 238176a..da7ad8b 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1391,252 +1391,204 @@ func TestScramAuth_handleServerFirstResponse(t *testing.T) { t.Errorf("expected error to be %q, got %q", expectedErr, err) } }) - } -/* +func TestCRAMMD5Auth(t *testing.T) { + t.Run("CRAM-MD5 on test server succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH CRAM-MD5\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + auth := CRAMMD5Auth("username", "password") + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + if err = client.Auth(auth); err != nil { + t.Errorf("failed to auth to test server: %s", err) + } + }) + t.Run("CRAM-MD5 on test server fails", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH CRAM-MD5\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) - - - - -func TestAuthSCRAMSHA1_OK(t *testing.T) { - hostname := "127.0.0.1" - port := "2585" - - go func() { - startSMTPServer(false, hostname, port, sha1.New) - }() - time.Sleep(time.Millisecond * 500) - - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - if err = client.Auth(ScramSHA1Auth("username", "password")); err != nil { - t.Errorf("failed to authenticate: %v", err) - } + auth := CRAMMD5Auth("username", "password") + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + if err = client.Auth(auth); err == nil { + t.Error("auth should fail on test server") + } + }) } -func TestAuthSCRAMSHA256_OK(t *testing.T) { - hostname := "127.0.0.1" - port := "2586" +func TestNewClient(t *testing.T) { + t.Run("new client via Dial succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) - go func() { - startSMTPServer(false, hostname, port, sha256.New) - }() - time.Sleep(time.Millisecond * 500) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to create client: %s", err) + } + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + t.Run("new client via Dial fails on server not started", func(t *testing.T) { + _, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, 64000)) + if err == nil { + t.Error("dial on non-existant server should fail") + } + }) + t.Run("new client fails on server not available", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDial: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - if err = client.Auth(ScramSHA256Auth("username", "password")); err != nil { - t.Errorf("failed to authenticate: %v", err) - } + _, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err == nil { + t.Error("connection to non-available server should fail") + } + }) + t.Run("new client fails on faker that fails on close", func(t *testing.T) { + server := "442 service not available\r\n" + var wrote strings.Builder + var fake faker + fake.failOnClose = true + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(server), + &wrote, + } + _, err := NewClient(fake, "faker.host") + if err == nil { + t.Error("connection to non-available server should fail on close") + } + }) } -func TestAuthSCRAMSHA1PLUS_OK(t *testing.T) { - hostname := "127.0.0.1" - port := "2590" +func TestClient_hello(t *testing.T) { + t.Run("client fails on EHLO but not on HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) - go func() { - startSMTPServer(true, hostname, port, sha1.New) - }() - time.Sleep(time.Millisecond * 500) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} - - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - - tlsConnState := conn.ConnectionState() - if err = client.Auth(ScramSHA1PlusAuth("username", "password", &tlsConnState)); err != nil { - t.Errorf("failed to authenticate: %v", err) - } + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.hello(); err == nil { + t.Error("helo should fail on test server") + } + }) } -func TestAuthSCRAMSHA256PLUS_OK(t *testing.T) { - hostname := "127.0.0.1" - port := "2591" +func TestClient_Hello(t *testing.T) { + t.Run("normal client HELO/EHLO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) - go func() { - startSMTPServer(true, hostname, port, sha256.New) - }() - time.Sleep(time.Millisecond * 500) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} - - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - - tlsConnState := conn.ConnectionState() - if err = client.Auth(ScramSHA256PlusAuth("username", "password", &tlsConnState)); err != nil { - t.Errorf("failed to authenticate: %v", err) - } -} - -func TestAuthSCRAMSHA1_fail(t *testing.T) { - hostname := "127.0.0.1" - port := "2587" - - go func() { - startSMTPServer(false, hostname, port, sha1.New) - }() - time.Sleep(time.Millisecond * 500) - - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - if err = client.Auth(ScramSHA1Auth("username", "invalid")); err == nil { - t.Errorf("expected auth error, got nil") - } -} - -func TestAuthSCRAMSHA256_fail(t *testing.T) { - hostname := "127.0.0.1" - port := "2588" - - go func() { - startSMTPServer(false, hostname, port, sha256.New) - }() - time.Sleep(time.Millisecond * 500) - - conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port)) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - if err = client.Auth(ScramSHA256Auth("username", "invalid")); err == nil { - t.Errorf("expected auth error, got nil") - } -} - -func TestAuthSCRAMSHA1PLUS_fail(t *testing.T) { - hostname := "127.0.0.1" - port := "2592" - - go func() { - startSMTPServer(true, hostname, port, sha1.New) - }() - time.Sleep(time.Millisecond * 500) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} - - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - tlsConnState := conn.ConnectionState() - if err = client.Auth(ScramSHA1PlusAuth("username", "invalid", &tlsConnState)); err == nil { - t.Errorf("expected auth error, got nil") - } -} - -func TestAuthSCRAMSHA256PLUS_fail(t *testing.T) { - hostname := "127.0.0.1" - port := "2593" - - go func() { - startSMTPServer(true, hostname, port, sha1.New) - }() - time.Sleep(time.Millisecond * 500) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} - - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", hostname, port), &tlsConfig) - if err != nil { - t.Errorf("failed to dial server: %v", err) - } - client, err := NewClient(conn, hostname) - if err != nil { - t.Errorf("failed to create client: %v", err) - } - if err = client.Hello(hostname); err != nil { - t.Errorf("failed to send HELO: %v", err) - } - tlsConnState := conn.ConnectionState() - if err = client.Auth(ScramSHA256PlusAuth("username", "invalid", &tlsConnState)); err == nil { - t.Errorf("expected auth error, got nil") - } + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Hello(TestServerAddr); err != nil { + t.Errorf("failed to send HELO/EHLO to test server: %s", err) + } + }) } // Issue 17794: don't send a trailing space on AUTH command when there's no password. -func TestClientAuthTrimSpace(t *testing.T) { +func TestClient_Auth_trimSpace(t *testing.T) { server := "220 hello world\r\n" + "200 some more" var wrote strings.Builder @@ -1655,7 +1607,7 @@ func TestClientAuthTrimSpace(t *testing.T) { c.tls = true c.didHello = true _ = c.Auth(toServerEmptyAuth{}) - if err := c.Close(); err != nil { + if err = c.Close(); err != nil { t.Errorf("close failed: %s", err) } if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want { @@ -1663,19 +1615,15 @@ func TestClientAuthTrimSpace(t *testing.T) { } } -// toServerEmptyAuth is an implementation of Auth that only implements -// the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns -// zero bytes for "toServer" so we can test that we don't send spaces at -// the end of the line. See TestClientAuthTrimSpace. -type toServerEmptyAuth struct{} +/* + + + + + + -func (toServerEmptyAuth) Start(_ *ServerInfo) (proto string, toServer []byte, err error) { - return "FOOAUTH", nil, nil -} -func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) { - panic("unexpected call") -} func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") @@ -3158,46 +3106,6 @@ func sendMail(hostPort string) error { return SendMail(hostPort, nil, from, to, []byte("Subject: test\n\nhowdy!")) } -// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: -// -// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ -// --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h -var localhostCert = []byte(` ------BEGIN CERTIFICATE----- -MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw -EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 -MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa -JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30 -LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw -DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF -MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA -AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi -NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4 -n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF -tN8URjVmyEo= ------END CERTIFICATE-----`) - -// localhostKey is the private key for localhostCert. -var localhostKey = []byte(testingKey(` ------BEGIN RSA TESTING KEY----- -MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h -AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu -lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB -AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA -kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM -VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m -542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb -PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2 -6jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB -vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP -QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i -jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c -qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg== ------END RSA TESTING KEY-----`)) - -func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } - var flaky = flag.Bool("flaky", false, "run known-flaky tests too") func SkipFlaky(t testing.TB, issue int) { @@ -3207,271 +3115,22 @@ func SkipFlaky(t testing.TB, issue int) { } } -// testSCRAMSMTPServer represents a test server for SCRAM-based SMTP authentication. -// It does not do any acutal computation of the challenges but verifies that the expected -// fields are present. We have actual real authentication tests for all SCRAM modes in the -// go-mail client_test.go -type testSCRAMSMTPServer struct { - authMechanism string - nonce string - hostname string - port string - tlsServer bool - h func() hash.Hash -} - -func (s *testSCRAMSMTPServer) handleConnection(conn net.Conn) { - defer func() { - _ = conn.Close() - }() - - reader := bufio.NewReader(conn) - writer := bufio.NewWriter(conn) - 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 { - return - } - - data, err := reader.ReadString('\n') - if err != nil { - return - } - data = strings.TrimSpace(data) - if strings.HasPrefix(data, "EHLO") { - _ = writeLine(fmt.Sprintf("250-%s", s.hostname)) - _ = writeLine("250-AUTH SCRAM-SHA-1 SCRAM-SHA-256") - writeOK() - } else { - _ = writeLine("500 Invalid command") - return - } - - for { - data, err = reader.ReadString('\n') - if err != nil { - fmt.Printf("failed to read data: %v", err) - } - data = strings.TrimSpace(data) - if strings.HasPrefix(data, "AUTH") { - parts := strings.Split(data, " ") - if len(parts) < 2 { - _ = writeLine("500 Syntax error") - return - } - - authMechanism := parts[1] - if authMechanism != "SCRAM-SHA-1" && authMechanism != "SCRAM-SHA-256" && - authMechanism != "SCRAM-SHA-1-PLUS" && authMechanism != "SCRAM-SHA-256-PLUS" { - _ = writeLine("504 Unrecognized authentication mechanism") - return - } - s.authMechanism = authMechanism - _ = writeLine("334 ") - s.handleSCRAMAuth(conn) - return - } else { - _ = writeLine("500 Invalid command") - } - } -} - -func (s *testSCRAMSMTPServer) handleSCRAMAuth(conn net.Conn) { - reader := bufio.NewReader(conn) - writer := bufio.NewWriter(conn) - 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() - } - var authMsg string - - data, err := reader.ReadString('\n') - if err != nil { - _ = writeLine("535 Authentication failed") - return - } - data = strings.TrimSpace(data) - decodedMessage, err := base64.StdEncoding.DecodeString(data) - if err != nil { - _ = writeLine("535 Authentication failed") - return - } - splits := strings.Split(string(decodedMessage), ",") - if len(splits) != 4 { - _ = writeLine("535 Authentication failed - expected 4 parts") - return - } - if !s.tlsServer && splits[0] != "n" { - _ = writeLine("535 Authentication failed - expected n to be in the first part") - return - } - if s.tlsServer && !strings.HasPrefix(splits[0], "p=") { - _ = writeLine("535 Authentication failed - expected p= to be in the first part") - return - } - if splits[2] != "n=username" { - _ = writeLine("535 Authentication failed - expected n=username to be in the third part") - return - } - if !strings.HasPrefix(splits[3], "r=") { - _ = writeLine("535 Authentication failed - expected r= to be in the fourth part") - return - } - authMsg = splits[2] + "," + splits[3] - - clientNonce := s.extractNonce(string(decodedMessage)) - if clientNonce == "" { - _ = writeLine("535 Authentication failed") - return - } - - s.nonce = clientNonce + "server_nonce" - serverFirstMessage := fmt.Sprintf("r=%s,s=%s,i=4096", s.nonce, - base64.StdEncoding.EncodeToString([]byte("salt"))) - _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFirstMessage)))) - authMsg = authMsg + "," + serverFirstMessage - - data, err = reader.ReadString('\n') - if err != nil { - _ = writeLine("535 Authentication failed") - return - } - data = strings.TrimSpace(data) - decodedFinalMessage, err := base64.StdEncoding.DecodeString(data) - if err != nil { - _ = writeLine("535 Authentication failed") - return - } - splits = strings.Split(string(decodedFinalMessage), ",") - - if !s.tlsServer && splits[0] != "c=biws" { - _ = writeLine("535 Authentication failed - expected c=biws to be in the first part") - return - } - if s.tlsServer { - if !strings.HasPrefix(splits[0], "c=") { - _ = writeLine("535 Authentication failed - expected c= to be in the first part") - return - } - channelBind, err := base64.StdEncoding.DecodeString(splits[0][2:]) - if err != nil { - _ = writeLine("535 Authentication failed - base64 channel bind is not valid - " + err.Error()) - return - } - if !strings.HasPrefix(string(channelBind), "p=") { - _ = writeLine("535 Authentication failed - expected channel binding to start with p=-") - return - } - cbType := string(channelBind[2:]) - if !strings.HasPrefix(cbType, "tls-unique") && !strings.HasPrefix(cbType, "tls-exporter") { - _ = writeLine("535 Authentication failed - expected channel binding type tls-unique or tls-exporter") - return - } - } - - if !strings.HasPrefix(splits[1], "r=") { - _ = writeLine("535 Authentication failed - expected r to be in the second part") - return - } - if !strings.Contains(splits[1], "server_nonce") { - _ = writeLine("535 Authentication failed - expected server_nonce to be in the second part") - return - } - if !strings.HasPrefix(splits[2], "p=") { - _ = writeLine("535 Authentication failed - expected p to be in the third part") - return - } - - authMsg = authMsg + "," + splits[0] + "," + splits[1] - saltedPwd := pbkdf2.Key([]byte("password"), []byte("salt"), 4096, s.h().Size(), s.h) - mac := hmac.New(s.h, saltedPwd) - mac.Write([]byte("Server Key")) - skey := mac.Sum(nil) - mac.Reset() - - mac = hmac.New(s.h, skey) - mac.Write([]byte(authMsg)) - ssig := mac.Sum(nil) - mac.Reset() - - serverFinalMessage := fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(ssig)) - _ = writeLine(fmt.Sprintf("334 %s", base64.StdEncoding.EncodeToString([]byte(serverFinalMessage)))) - - _, err = reader.ReadString('\n') - if err != nil { - _ = writeLine("535 Authentication failed") - return - } - - _ = writeLine("235 Authentication successful") -} - -func (s *testSCRAMSMTPServer) extractNonce(message string) string { - parts := strings.Split(message, ",") - for _, part := range parts { - if strings.HasPrefix(part, "r=") { - return part[2:] - } - } - return "" -} - -func startSMTPServer(tlsServer bool, hostname, port string, h func() hash.Hash) { - server := &testSCRAMSMTPServer{ - hostname: hostname, - port: port, - tlsServer: tlsServer, - h: h, - } - listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", hostname, port)) - if err != nil { - fmt.Printf("Failed to start SMTP server: %v", err) - } - defer func() { - _ = listener.Close() - }() - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}} - - for { - conn, err := listener.Accept() - if err != nil { - fmt.Printf("Failed to accept connection: %v", err) - continue - } - if server.tlsServer { - conn = tls.Server(conn, &tlsConfig) - } - go server.handleConnection(conn) - } -} - */ // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter + failOnRead bool + failOnClose bool } -func (f faker) Close() error { return nil } +func (f faker) Close() error { + if f.failOnClose { + return fmt.Errorf("faker: failed to close connection") + } + return nil +} func (f faker) LocalAddr() net.Addr { return nil } func (f faker) RemoteAddr() net.Addr { return nil } func (f faker) SetDeadline(time.Time) error { return nil } @@ -3486,6 +3145,8 @@ type serverProps struct { FailOnAuth bool FailOnDataInit bool FailOnDataClose bool + FailOnDial bool + FailOnEhlo bool FailOnHelo bool FailOnMailFrom bool FailOnNoop bool @@ -3582,6 +3243,10 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } if !props.IsTLS { + if props.FailOnDial { + writeLine("421 4.4.1 Service not available") + return + } writeLine("220 go-mail test server ready ESMTP") } @@ -3595,9 +3260,9 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server var datastring string data = strings.TrimSpace(data) switch { - case strings.HasPrefix(data, "EHLO"), strings.HasPrefix(data, "HELO"): + case strings.HasPrefix(data, "HELO"): if len(strings.Split(data, " ")) != 2 { - writeLine("501 Syntax: EHLO hostname") + writeLine("501 Syntax: HELO hostname") break } if props.FailOnHelo { @@ -3605,6 +3270,16 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + case strings.HasPrefix(data, "EHLO"): + if len(strings.Split(data, " ")) != 2 { + writeLine("501 Syntax: EHLO hostname") + break + } + if props.FailOnEhlo { + writeLine("500 5.5.2 Error: fail on EHLO") + break + } + writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) case strings.HasPrefix(data, "MAIL FROM:"): if props.FailOnMailFrom { writeLine("500 5.5.2 Error: fail on MAIL FROM") @@ -3886,3 +3561,17 @@ type randReader struct{} func (r *randReader) Read([]byte) (int, error) { return 0, errors.New("broken reader") } + +// toServerEmptyAuth is an implementation of Auth that only implements +// the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns +// zero bytes for "toServer" so we can test that we don't send spaces at +// the end of the line. See TestClientAuthTrimSpace. +type toServerEmptyAuth struct{} + +func (toServerEmptyAuth) Start(_ *ServerInfo) (proto string, toServer []byte, err error) { + return "FOOAUTH", nil, nil +} + +func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) { + return nil, fmt.Errorf("unexpected call") +} From af7964450a47848ab3c2731789561ee46580fd04 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 9 Nov 2024 14:01:02 +0100 Subject: [PATCH 026/125] Add test cases for invalid HELO/EHLO commands Add tests to ensure HELO/EHLO commands fail with empty name, newline in name, and double execution. This improves validation and robustness of the SMTP client implementation. --- smtp/smtp_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index da7ad8b..194315f 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1585,6 +1585,87 @@ func TestClient_Hello(t *testing.T) { t.Errorf("failed to send HELO/EHLO to test server: %s", err) } }) + t.Run("client HELO/EHLO with empty name should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Hello(""); err == nil { + t.Error("HELO/EHLO with empty name should fail") + } + }) + t.Run("client HELO/EHLO with newline in name should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Hello(TestServerAddr + "\r\n"); err == nil { + t.Error("HELO/EHLO with newline should fail") + } + }) + t.Run("client double HELO/EHLO should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Hello(TestServerAddr); err != nil { + t.Errorf("failed to send HELO/EHLO to test server: %s", err) + } + if err = client.Hello(TestServerAddr); err == nil { + t.Error("double HELO/EHLO should fail") + } + }) } // Issue 17794: don't send a trailing space on AUTH command when there's no password. From 50505e1339a1fb3ea2cf17f1b868bb5b0da70db8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 9 Nov 2024 14:26:44 +0100 Subject: [PATCH 027/125] Add test for Client cmd failure on textproto command This commit introduces a new test case for the `Client`'s `cmd` method to ensure it fails correctly when the `textproto` command encounters a broken writer. It also adds a `failWriter` struct that simulates a write failure to facilitate this testing scenario. --- smtp/smtp_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 194315f..d7a2a43 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1668,6 +1668,29 @@ func TestClient_Hello(t *testing.T) { }) } +func TestClient_cmd(t *testing.T) { + t.Run("cmd fails on textproto cmd", func(t *testing.T) { + server := "220 server ready\r\n" + var fake faker + fake.failOnClose = true + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(server), + &failWriter{}, + } + client, err := NewClient(fake, "faker.host") + if err != nil { + t.Errorf("failed to create client: %s", err) + } + _, _, err = client.cmd(250, "HELO faker.host") + if err == nil { + t.Error("cmd should fail on textproto cmd with broken writer") + } + }) +} + // Issue 17794: don't send a trailing space on AUTH command when there's no password. func TestClient_Auth_trimSpace(t *testing.T) { server := "220 hello world\r\n" + @@ -3656,3 +3679,10 @@ func (toServerEmptyAuth) Start(_ *ServerInfo) (proto string, toServer []byte, er func (toServerEmptyAuth) Next(_ []byte, _ bool) (toServer []byte, err error) { return nil, fmt.Errorf("unexpected call") } + +// failWriter is a struct type that implements the io.Writer interface, but always returns an error on Write. +type failWriter struct{} + +func (w *failWriter) Write([]byte) (int, error) { + return 0, errors.New("broken writer") +} From 8f28babc473bf970567ad7cf99fcfb255c00e001 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 9 Nov 2024 14:58:23 +0100 Subject: [PATCH 028/125] Add tests for Client StartTLS functionality Introduce new tests to cover the Client's behavior when initiating a STARTTLS session under different conditions: normal operation, failure on EHLO/HELO, and a server not supporting STARTTLS. This ensures robustness in handling STARTTLS interactions. --- smtp/smtp_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index d7a2a43..9dc3c10 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1691,6 +1691,108 @@ func TestClient_cmd(t *testing.T) { }) } +func TestClient_StartTLS(t *testing.T) { + t.Run("normal STARTTLS should succeed", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + tlsConfig := &tls.Config{InsecureSkipVerify: true} + if err = client.StartTLS(tlsConfig); err != nil { + t.Errorf("failed to initialize STARTTLS session: %s", err) + } + }) + t.Run("STARTTLS fails on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + tlsConfig := &tls.Config{InsecureSkipVerify: true} + if err = client.StartTLS(tlsConfig); err == nil { + t.Error("STARTTLS should fail on EHLO") + } + }) + t.Run("STARTTLS fails on server not supporting STARTTLS", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnSTARTTLS: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + tlsConfig := &tls.Config{InsecureSkipVerify: true} + if err = client.StartTLS(tlsConfig); err == nil { + t.Error("STARTTLS should fail for server not supporting it") + } + }) +} + // Issue 17794: don't send a trailing space on AUTH command when there's no password. func TestClient_Auth_trimSpace(t *testing.T) { server := "220 hello world\r\n" + From b7ffce62aafa76e84d54e46aa8a7b7fab6d3b513 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 9 Nov 2024 15:22:23 +0100 Subject: [PATCH 029/125] Add TLS connection state tests for SMTP client Introduce tests to verify TLS connection state handling in the SMTP client. Ensure that normal TLS connections return a valid state, and non-TLS connections do not wrongly indicate a TLS state. --- smtp/smtp_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 9dc3c10..6281666 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1793,6 +1793,80 @@ func TestClient_StartTLS(t *testing.T) { }) } +func TestClient_TLSConnectionState(t *testing.T) { + t.Run("normal TLS connection should return a state", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + tlsConfig := &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12} + if err = client.StartTLS(tlsConfig); err != nil { + t.Errorf("failed to initialize STARTTLS session: %s", err) + } + state, ok := client.TLSConnectionState() + if !ok { + t.Errorf("failed to get TLS connection state") + } + if state.Version < tls.VersionTLS12 { + t.Errorf("TLS connection state version is %d, should be >= %d", state.Version, tls.VersionTLS12) + } + }) + t.Run("no TLS state on non-TLS connection", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + _, ok := client.TLSConnectionState() + if ok { + t.Error("non-TLS connection should not have TLS connection state") + } + }) +} + // Issue 17794: don't send a trailing space on AUTH command when there's no password. func TestClient_Auth_trimSpace(t *testing.T) { server := "220 hello world\r\n" + From 0df228178a77121bd836cdc2223ca75a309d5575 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 10 Nov 2024 14:06:05 +0100 Subject: [PATCH 030/125] Add extensive client tests for Mail, Verify, and Auth commands Introduce new tests for the SMTP client covering scenarios for Mail, Verify, and Auth commands to ensure correct behavior under various conditions. Updated `simpleSMTPServer` implementation to handle more cases including VRFY and SMTPUTF8. --- smtp/smtp_test.go | 494 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 436 insertions(+), 58 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 6281666..a6185d5 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1867,44 +1867,408 @@ func TestClient_TLSConnectionState(t *testing.T) { }) } -// Issue 17794: don't send a trailing space on AUTH command when there's no password. -func TestClient_Auth_trimSpace(t *testing.T) { - server := "220 hello world\r\n" + - "200 some more" - var wrote strings.Builder - var fake faker - fake.ReadWriter = struct { - io.Reader - io.Writer - }{ - strings.NewReader(server), - &wrote, - } - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v", err) - } - c.tls = true - c.didHello = true - _ = c.Auth(toServerEmptyAuth{}) - if err = c.Close(); err != nil { - t.Errorf("close failed: %s", err) - } - if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want { - t.Errorf("wrote %q; want %q", got, want) - } +func TestClient_Verify(t *testing.T) { + t.Run("Verify on existing user succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Verify("toni.tester@example.com"); err != nil { + t.Errorf("failed to verify user: %s", err) + } + }) + t.Run("Verify on non-existing user fails", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + VRFYUserUnknown: true, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Verify("toni.tester@example.com"); err == nil { + t.Error("verify on non-existing user should fail") + } + }) + t.Run("Verify with newlines should fails", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Verify("toni.tester@example.com\r\n"); err == nil { + t.Error("verify with new lines should fail") + } + }) + t.Run("Verify should fail on HELO/EHLO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Verify("toni.tester@example.com"); err == nil { + t.Error("verify with new lines should fail") + } + }) +} + +func TestClient_Auth(t *testing.T) { + t.Run("Auth fails on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err = client.Auth(auth); err == nil { + t.Error("auth should fail on EHLO/HELO") + } + }) + t.Run("Auth fails on auth-start", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + auth := LoginAuth("username", "password", "not.localhost.com", false) + if err = client.Auth(auth); err == nil { + t.Error("auth should fail on auth-start, then on quit") + } + expErr := "wrong host name" + if !strings.EqualFold(expErr, err.Error()) { + t.Errorf("expected error: %q, got: %q", expErr, err.Error()) + } + }) + t.Run("Auth fails on auth-start and then on quit", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-STARTTLS\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FailOnQuit: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + auth := LoginAuth("username", "password", "not.localhost.com", false) + if err = client.Auth(auth); err == nil { + t.Error("auth should fail on auth-start, then on quit") + } + expErr := "wrong host name, 500 5.1.2 Error: quit failed" + if !strings.EqualFold(expErr, err.Error()) { + t.Errorf("expected error: %q, got: %q", expErr, err.Error()) + } + }) + // Issue 17794: don't send a trailing space on AUTH command when there's no password. + t.Run("No trailing space on AUTH when there is no password (Issue 17794)", func(t *testing.T) { + server := "220 hello world\r\n" + + "200 some more" + var wrote strings.Builder + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + strings.NewReader(server), + &wrote, + } + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v", err) + } + c.tls = true + c.didHello = true + _ = c.Auth(toServerEmptyAuth{}) + if err = c.Close(); err != nil { + t.Errorf("close failed: %s", err) + } + if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want { + t.Errorf("wrote %q; want %q", got, want) + } + }) +} + +func TestClient_Mail(t *testing.T) { + t.Run("normal from address succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Mail("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set mail from address: %s", err) + } + }) + t.Run("from address with new lines fails", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Mail("valid-from@domain.tld\r\n"); err == nil { + t.Error("mail from address with new lines should fail") + } + }) + t.Run("from address fails on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Mail("valid-from@domain.tld"); err == nil { + t.Error("mail from address should fail on EHLO/HELO") + } + }) + t.Run("from address and server supports 8BITMIME", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoCommandAsError: "MAIL FROM:", + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Mail("valid-from@domain.tld"); err == nil { + t.Error("server should echo the command as error but didn't") + } + sent := strings.Replace(err.Error(), "500 ", "", -1) + expected := "MAIL FROM: BODY=8BITMIME" + if !strings.EqualFold(sent, expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + } + }) + t.Run("from address and server supports SMTPUTF8", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-SMTPUTF8\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoCommandAsError: "MAIL FROM:", + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + if err = client.Mail("valid-from@domain.tld"); err == nil { + t.Error("server should echo the command as error but didn't") + } + sent := strings.Replace(err.Error(), "500 ", "", -1) + expected := "MAIL FROM: SMTPUTF8" + if !strings.EqualFold(sent, expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + } + }) } /* - - - - - - - - - func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") @@ -3422,26 +3786,28 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { - FailOnAuth bool - FailOnDataInit bool - FailOnDataClose bool - FailOnDial bool - FailOnEhlo bool - FailOnHelo bool - FailOnMailFrom bool - FailOnNoop bool - FailOnQuit bool - FailOnReset bool - FailOnSTARTTLS bool - FailTemp bool - FeatureSet string - ListenPort int - SSLListener bool - IsSCRAMPlus bool - IsTLS bool - SupportDSN bool - TestSCRAM bool - HashFunc func() hash.Hash + EchoCommandAsError string + FailOnAuth bool + FailOnDataInit bool + FailOnDataClose bool + FailOnDial bool + FailOnEhlo bool + FailOnHelo bool + FailOnMailFrom bool + FailOnNoop bool + FailOnQuit bool + FailOnReset bool + FailOnSTARTTLS bool + FailTemp bool + FeatureSet string + ListenPort int + HashFunc func() hash.Hash + IsSCRAMPlus bool + IsTLS bool + SupportDSN bool + SSLListener bool + TestSCRAM bool + VRFYUserUnknown bool } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -3540,6 +3906,9 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server var datastring string data = strings.TrimSpace(data) switch { + case props.EchoCommandAsError != "" && strings.HasPrefix(data, props.EchoCommandAsError): + writeLine("500 " + data) + break case strings.HasPrefix(data, "HELO"): if len(strings.Split(data, " ")) != 2 { writeLine("501 Syntax: HELO hostname") @@ -3643,8 +4012,17 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } writeOK() - case strings.EqualFold(data, "vrfy"): - writeOK() + case strings.HasPrefix(data, "VRFY"): + if props.VRFYUserUnknown { + writeLine("550 5.1.1 User unknown") + break + } + parts := strings.SplitN(data, " ", 2) + if len(parts) != 2 { + writeLine("500 5.0.0 Error: invalid syntax for VRFY") + break + } + writeLine(fmt.Sprintf("250 2.0.0 Ok: %s OK", parts[1])) case strings.EqualFold(data, "rset"): if props.FailOnReset { writeLine("500 5.1.2 Error: reset failed") @@ -3674,7 +4052,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server props.IsTLS = true handleTestServerConnection(connection, t, props) default: - writeLine("500 5.5.2 Error: bad syntax") + writeLine("500 5.5.2 Error: bad syntax - " + data) } } } From 0d8d097ae1a5ac8ed739abb2253e53606baa8ade Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 10 Nov 2024 14:10:50 +0100 Subject: [PATCH 031/125] Add tests for DSN, 8BITMIME, and SMTPUTF8 support in SMTP Introduce new test cases to verify the SMTP server's ability to handle DSN, 8BITMIME, and SMTPUTF8 features. These tests ensure correct response behavior when these features are supported by the server. --- smtp/smtp_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index a6185d5..bf31b05 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -2266,6 +2266,72 @@ func TestClient_Mail(t *testing.T) { t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) } }) + t.Run("from address and server supports DSN", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoCommandAsError: "MAIL FROM:", + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + client.dsnmrtype = "FULL" + if err = client.Mail("valid-from@domain.tld"); err == nil { + t.Error("server should echo the command as error but didn't") + } + sent := strings.Replace(err.Error(), "500 ", "", -1) + expected := "MAIL FROM: RET=FULL" + if !strings.EqualFold(sent, expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + } + }) + t.Run("from address and server supports DSN, SMTPUTF8 and 8BITMIME", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250-8BITMIME\r\n250-SMTPUTF8\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoCommandAsError: "MAIL FROM:", + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Errorf("failed to dial to test server: %s", err) + } + client.dsnmrtype = "FULL" + if err = client.Mail("valid-from@domain.tld"); err == nil { + t.Error("server should echo the command as error but didn't") + } + sent := strings.Replace(err.Error(), "500 ", "", -1) + expected := "MAIL FROM: BODY=8BITMIME SMTPUTF8 RET=FULL" + if !strings.EqualFold(sent, expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + } + }) } /* From d446b491e2dfbb0f23315080f15014de5c756e11 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 10 Nov 2024 14:31:18 +0100 Subject: [PATCH 032/125] Add client cleanup to SMTP tests and new TestClient_Rcpt Update SMTP tests to use t.Cleanup for client cleanup to ensure proper resource release. Introduce a new test, TestClient_Rcpt, to verify recipient address handling under various conditions. --- smtp/smtp_test.go | 152 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 145 insertions(+), 7 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index bf31b05..a999f4c 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -2142,8 +2142,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) if err = client.Mail("valid-from@domain.tld"); err != nil { t.Errorf("failed to set mail from address: %s", err) } @@ -2168,8 +2173,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) if err = client.Mail("valid-from@domain.tld\r\n"); err == nil { t.Error("mail from address with new lines should fail") } @@ -2196,8 +2206,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("mail from address should fail on EHLO/HELO") } @@ -2223,8 +2238,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("server should echo the command as error but didn't") } @@ -2255,8 +2275,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("server should echo the command as error but didn't") } @@ -2287,8 +2312,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) client.dsnmrtype = "FULL" if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("server should echo the command as error but didn't") @@ -2320,8 +2350,13 @@ func TestClient_Mail(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) client.dsnmrtype = "FULL" if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("server should echo the command as error but didn't") @@ -2334,6 +2369,109 @@ func TestClient_Mail(t *testing.T) { }) } +func TestClient_Rcpt(t *testing.T) { + t.Run("normal recipient address succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250-8BITMIME\r\n250-SMTPUTF8\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Rcpt("valid-to@domain.tld"); err != nil { + t.Errorf("failed to set recipient address: %s", err) + } + }) + t.Run("recipient address with newlines fails", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Rcpt("valid-to@domain.tld\r\n"); err == nil { + t.Error("recpient address with newlines should fail") + } + }) + t.Run("recipient address with DSN", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoCommandAsError: "RCPT TO:", + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Hello(TestServerAddr); err != nil { + t.Fatalf("failed to send hello to test server: %s", err) + } + client.dsnrntype = "SUCCESS" + if err = client.Rcpt("valid-to@domain.tld"); err == nil { + t.Error("recpient address with newlines should fail") + } + sent := strings.Replace(err.Error(), "500 ", "", -1) + expected := "RCPT TO: NOTIFY=SUCCESS" + if !strings.EqualFold(sent, expected) { + t.Errorf("expected rcpt to command to be %q, but sent %q", expected, sent) + } + }) +} + /* func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") From 75bfdd2855390742da580fafa08454b5c9e76437 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 12:28:52 +0100 Subject: [PATCH 033/125] Update smtp tests to use t.Fatalf for critical failures Changed `t.Errorf` to `t.Fatalf` in multiple instances within the test cases. This ensures that the tests halt immediately upon a critical failure, improving test reliability and debugging clarity. --- smtp/smtp_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index a999f4c..66d3ee5 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1472,7 +1472,7 @@ func TestNewClient(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to create client: %s", err) + t.Fatalf("failed to create client: %s", err) } if err := client.Close(); err != nil { t.Errorf("failed to close client: %s", err) @@ -1550,7 +1550,7 @@ func TestClient_hello(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } if err = client.hello(); err == nil { t.Error("helo should fail on test server") @@ -1579,7 +1579,7 @@ func TestClient_Hello(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } if err = client.Hello(TestServerAddr); err != nil { t.Errorf("failed to send HELO/EHLO to test server: %s", err) @@ -1605,7 +1605,7 @@ func TestClient_Hello(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } if err = client.Hello(""); err == nil { t.Error("HELO/EHLO with empty name should fail") @@ -1657,7 +1657,7 @@ func TestClient_Hello(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } if err = client.Hello(TestServerAddr); err != nil { t.Errorf("failed to send HELO/EHLO to test server: %s", err) @@ -1712,7 +1712,7 @@ func TestClient_StartTLS(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1746,7 +1746,7 @@ func TestClient_StartTLS(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1779,7 +1779,7 @@ func TestClient_StartTLS(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1814,7 +1814,7 @@ func TestClient_TLSConnectionState(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1853,7 +1853,7 @@ func TestClient_TLSConnectionState(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1888,7 +1888,7 @@ func TestClient_Verify(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1920,7 +1920,7 @@ func TestClient_Verify(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1951,7 +1951,7 @@ func TestClient_Verify(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -1984,7 +1984,7 @@ func TestClient_Verify(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } t.Cleanup(func() { if err = client.Close(); err != nil { @@ -2020,7 +2020,7 @@ func TestClient_Auth(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } auth := LoginAuth("username", "password", TestServerAddr, false) if err = client.Auth(auth); err == nil { @@ -2048,7 +2048,7 @@ func TestClient_Auth(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } auth := LoginAuth("username", "password", "not.localhost.com", false) if err = client.Auth(auth); err == nil { @@ -2081,7 +2081,7 @@ func TestClient_Auth(t *testing.T) { client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { - t.Errorf("failed to dial to test server: %s", err) + t.Fatalf("failed to dial to test server: %s", err) } auth := LoginAuth("username", "password", "not.localhost.com", false) if err = client.Auth(auth); err == nil { From e9c7bdbb4e963c5e489a020a77f7e6c2e64b8abc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 12:54:10 +0100 Subject: [PATCH 034/125] Refactor TLS config initialization in tests Replace repetitive TLS configuration code with a reusable `getTLSConfig` helper function for consistency and maintainability. Additionally, update port configuration and add new tests for mail data transmission. --- smtp/smtp_test.go | 120 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 66d3ee5..8613dd1 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -22,6 +22,7 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/tls" + "crypto/x509" "encoding/base64" "errors" "fmt" @@ -42,7 +43,7 @@ const ( // 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 = 12025 + TestServerPortBase = 30025 ) // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. @@ -1087,13 +1088,8 @@ func TestScramAuth(t *testing.T) { var client *Client switch tt.tls { case true: - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), &tlsConfig) + tlsConfig := getTLSConfig(t) + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), tlsConfig) if err != nil { t.Fatalf("failed to dial TLS server: %v", err) } @@ -1161,13 +1157,8 @@ func TestScramAuth(t *testing.T) { var client *Client switch tt.tls { case true: - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - fmt.Printf("error creating TLS cert: %s", err) - return - } - tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true} - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), &tlsConfig) + tlsConfig := getTLSConfig(t) + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", TestServerAddr, serverPort), tlsConfig) if err != nil { t.Fatalf("failed to dial TLS server: %v", err) } @@ -1719,7 +1710,7 @@ func TestClient_StartTLS(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - tlsConfig := &tls.Config{InsecureSkipVerify: true} + tlsConfig := getTLSConfig(t) if err = client.StartTLS(tlsConfig); err != nil { t.Errorf("failed to initialize STARTTLS session: %s", err) } @@ -1753,7 +1744,7 @@ func TestClient_StartTLS(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - tlsConfig := &tls.Config{InsecureSkipVerify: true} + tlsConfig := getTLSConfig(t) if err = client.StartTLS(tlsConfig); err == nil { t.Error("STARTTLS should fail on EHLO") } @@ -1786,7 +1777,7 @@ func TestClient_StartTLS(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - tlsConfig := &tls.Config{InsecureSkipVerify: true} + tlsConfig := getTLSConfig(t) if err = client.StartTLS(tlsConfig); err == nil { t.Error("STARTTLS should fail for server not supporting it") } @@ -1821,7 +1812,8 @@ func TestClient_TLSConnectionState(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - tlsConfig := &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12} + tlsConfig := getTLSConfig(t) + tlsConfig.MinVersion = tls.VersionTLS12 if err = client.StartTLS(tlsConfig); err != nil { t.Errorf("failed to initialize STARTTLS session: %s", err) } @@ -2472,6 +2464,79 @@ func TestClient_Rcpt(t *testing.T) { }) } +func TestClient_Data(t *testing.T) { + t.Run("normal mail data transmission succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + writer, err := client.Data() + if err != nil { + t.Fatalf("failed to create data writer: %s", err) + } + t.Cleanup(func() { + if err = writer.Close(); err != nil { + t.Errorf("failed to close data writer: %s", err) + } + }) + if _, err = writer.Write([]byte("test message")); err != nil { + t.Errorf("failed to write data to test server: %s", err) + } + }) + t.Run("mail data transmission fails on DATA command", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnDataInit: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if _, err = client.Data(); err == nil { + t.Error("expected data writer to fail") + } + }) +} + /* func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") @@ -4251,7 +4316,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } writeLine("220 Ready to start TLS") - tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}} + tlsConfig := &tls.Config{Certificates: []tls.Certificate{keypair}, ServerName: "example.com"} connection = tls.Server(connection, tlsConfig) props.IsTLS = true handleTestServerConnection(connection, t, props) @@ -4444,3 +4509,18 @@ type failWriter struct{} func (w *failWriter) Write([]byte) (int, error) { return 0, errors.New("broken writer") } + +func getTLSConfig(t *testing.T) *tls.Config { + t.Helper() + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("unable to load host certifcate: %s", err) + } + testRootCAs := x509.NewCertPool() + testRootCAs.AppendCertsFromPEM(localhostCert) + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: testRootCAs, + ServerName: "example.com", + } +} From 3fffcd15f618a8196f90871eba8b51aeaa71f18f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 12:54:39 +0100 Subject: [PATCH 035/125] Remove deprecated test hook for STARTTLS The testHookStartTLS variable and its related conditional code have been removed from the smtp.go file. This cleanup streamlines the TLS initiation process and removes unnecessary test-specific hooks no longer in use. --- smtp/smtp.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/smtp/smtp.go b/smtp/smtp.go index 4841ec8..943ca9d 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -432,8 +432,6 @@ func (c *Client) Data() (io.WriteCloser, error) { return datacloser, nil } -var testHookStartTLS func(*tls.Config) // nil, except for tests - // SendMail connects to the server at addr, switches to TLS if // possible, authenticates with the optional mechanism a if possible, // and then sends an email from address from, to addresses to, with @@ -475,9 +473,6 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error { } if ok, _ := c.Extension("STARTTLS"); ok { config := &tls.Config{ServerName: c.serverName} - if testHookStartTLS != nil { - testHookStartTLS(config) - } if err = c.StartTLS(config); err != nil { return err } From c3252626e37e3cd71ec0f82b31b55c29a1876b8e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 13:31:02 +0100 Subject: [PATCH 036/125] Reverted change made in 3fffcd15f618a8196f90871eba8b51aeaa71f18f --- smtp/smtp.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index 943ca9d..4841ec8 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -432,6 +432,8 @@ func (c *Client) Data() (io.WriteCloser, error) { return datacloser, nil } +var testHookStartTLS func(*tls.Config) // nil, except for tests + // SendMail connects to the server at addr, switches to TLS if // possible, authenticates with the optional mechanism a if possible, // and then sends an email from address from, to addresses to, with @@ -473,6 +475,9 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error { } if ok, _ := c.Extension("STARTTLS"); ok { config := &tls.Config{ServerName: c.serverName} + if testHookStartTLS != nil { + testHookStartTLS(config) + } if err = c.StartTLS(config); err != nil { return err } From c6da3936762d5e50bb8d7c1960f4e3a49dba6093 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 13:31:25 +0100 Subject: [PATCH 037/125] Add echo buffer to SMTP server tests Refactored SMTP server tests to use an echo buffer for capturing responses. This allows for better validation of command responses without relying on error messages. Additionally, added a new test case to validate a full SendMail transaction with TLS and authentication. --- smtp/smtp_test.go | 174 ++++++++++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 59 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 8613dd1..e7649f8 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -2215,11 +2215,12 @@ func TestClient_Mail(t *testing.T) { PortAdder.Add(1) serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-8BITMIME\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoCommandAsError: "MAIL FROM:", - FeatureSet: featureSet, - ListenPort: serverPort, + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, }, ); err != nil { t.Errorf("failed to start test server: %s", err) @@ -2237,13 +2238,13 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.Mail("valid-from@domain.tld"); err == nil { - t.Error("server should echo the command as error but didn't") + if err = client.Mail("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set mail from address: %s", err) } - sent := strings.Replace(err.Error(), "500 ", "", -1) expected := "MAIL FROM: BODY=8BITMIME" - if !strings.EqualFold(sent, expected) { - t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + resp := strings.Split(echoBuffer.String(), "\r\n") + if !strings.EqualFold(resp[5], expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } }) t.Run("from address and server supports SMTPUTF8", func(t *testing.T) { @@ -2252,11 +2253,12 @@ func TestClient_Mail(t *testing.T) { PortAdder.Add(1) serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-SMTPUTF8\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoCommandAsError: "MAIL FROM:", - FeatureSet: featureSet, - ListenPort: serverPort, + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, }, ); err != nil { t.Errorf("failed to start test server: %s", err) @@ -2274,13 +2276,13 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.Mail("valid-from@domain.tld"); err == nil { - t.Error("server should echo the command as error but didn't") + if err = client.Mail("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set mail from address: %s", err) } - sent := strings.Replace(err.Error(), "500 ", "", -1) expected := "MAIL FROM: SMTPUTF8" - if !strings.EqualFold(sent, expected) { - t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + resp := strings.Split(echoBuffer.String(), "\r\n") + if !strings.EqualFold(resp[5], expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } }) t.Run("from address and server supports DSN", func(t *testing.T) { @@ -2289,11 +2291,12 @@ func TestClient_Mail(t *testing.T) { PortAdder.Add(1) serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoCommandAsError: "MAIL FROM:", - FeatureSet: featureSet, - ListenPort: serverPort, + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, }, ); err != nil { t.Errorf("failed to start test server: %s", err) @@ -2315,10 +2318,10 @@ func TestClient_Mail(t *testing.T) { if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("server should echo the command as error but didn't") } - sent := strings.Replace(err.Error(), "500 ", "", -1) expected := "MAIL FROM: RET=FULL" - if !strings.EqualFold(sent, expected) { - t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + resp := strings.Split(echoBuffer.String(), "\r\n") + if !strings.EqualFold(resp[5], expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } }) t.Run("from address and server supports DSN, SMTPUTF8 and 8BITMIME", func(t *testing.T) { @@ -2327,11 +2330,12 @@ func TestClient_Mail(t *testing.T) { PortAdder.Add(1) serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250-8BITMIME\r\n250-SMTPUTF8\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoCommandAsError: "MAIL FROM:", - FeatureSet: featureSet, - ListenPort: serverPort, + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, }, ); err != nil { t.Errorf("failed to start test server: %s", err) @@ -2353,10 +2357,10 @@ func TestClient_Mail(t *testing.T) { if err = client.Mail("valid-from@domain.tld"); err == nil { t.Error("server should echo the command as error but didn't") } - sent := strings.Replace(err.Error(), "500 ", "", -1) expected := "MAIL FROM: BODY=8BITMIME SMTPUTF8 RET=FULL" - if !strings.EqualFold(sent, expected) { - t.Errorf("expected mail from command to be %q, but sent %q", expected, sent) + resp := strings.Split(echoBuffer.String(), "\r\n") + if !strings.EqualFold(resp[7], expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[7]) } }) } @@ -2428,11 +2432,12 @@ func TestClient_Rcpt(t *testing.T) { PortAdder.Add(1) serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoCommandAsError: "RCPT TO:", - FeatureSet: featureSet, - ListenPort: serverPort, + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, }, ); err != nil { t.Errorf("failed to start test server: %s", err) @@ -2456,10 +2461,10 @@ func TestClient_Rcpt(t *testing.T) { if err = client.Rcpt("valid-to@domain.tld"); err == nil { t.Error("recpient address with newlines should fail") } - sent := strings.Replace(err.Error(), "500 ", "", -1) expected := "RCPT TO: NOTIFY=SUCCESS" - if !strings.EqualFold(sent, expected) { - t.Errorf("expected rcpt to command to be %q, but sent %q", expected, sent) + resp := strings.Split(echoBuffer.String(), "\r\n") + if !strings.EqualFold(resp[5], expected) { + t.Errorf("expected rcpt to command to be %q, but sent %q", expected, resp[5]) } }) } @@ -2537,6 +2542,45 @@ func TestClient_Data(t *testing.T) { }) } +func TestSendMail(t *testing.T) { + t.Run("full SendMail transaction with TLS and auth", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, + []byte("test message")); err != nil { + t.Fatalf("failed to send mail: %s", err) + } + resp := strings.Split(echoBuffer.String(), "\r\n") + for i, line := range resp { + t.Logf("response line %d: %q", i, line) + } + }) +} + /* func TestBasic(t *testing.T) { server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") @@ -4055,28 +4099,28 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { - EchoCommandAsError string - FailOnAuth bool - FailOnDataInit bool - FailOnDataClose bool - FailOnDial bool - FailOnEhlo bool - FailOnHelo bool - FailOnMailFrom bool - FailOnNoop bool - FailOnQuit bool - FailOnReset bool - FailOnSTARTTLS bool - FailTemp bool - FeatureSet string - ListenPort int - HashFunc func() hash.Hash - IsSCRAMPlus bool - IsTLS bool - SupportDSN bool - SSLListener bool - TestSCRAM bool - VRFYUserUnknown bool + EchoBuffer io.Writer + FailOnAuth bool + FailOnDataInit bool + FailOnDataClose bool + FailOnDial bool + FailOnEhlo bool + FailOnHelo bool + FailOnMailFrom bool + FailOnNoop bool + FailOnQuit bool + FailOnReset bool + FailOnSTARTTLS bool + FailTemp bool + FeatureSet string + ListenPort int + HashFunc func() hash.Hash + IsSCRAMPlus bool + IsTLS bool + SupportDSN bool + SSLListener bool + TestSCRAM bool + VRFYUserUnknown bool } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -4151,6 +4195,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server if err != nil { t.Logf("failed to write line: %s", err) } + if props.EchoBuffer != nil { + if _, err := props.EchoBuffer.Write([]byte(data + "\r\n")); err != nil { + t.Errorf("failed write to echo buffer: %s", err) + } + } _ = writer.Flush() } writeOK := func() { @@ -4171,13 +4220,15 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } time.Sleep(time.Millisecond) + if props.EchoBuffer != nil { + if _, err = props.EchoBuffer.Write([]byte(data)); err != nil { + t.Errorf("failed write to echo buffer: %s", err) + } + } var datastring string data = strings.TrimSpace(data) switch { - case props.EchoCommandAsError != "" && strings.HasPrefix(data, props.EchoCommandAsError): - writeLine("500 " + data) - break case strings.HasPrefix(data, "HELO"): if len(strings.Split(data, " ")) != 2 { writeLine("501 Syntax: HELO hostname") @@ -4260,6 +4311,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server t.Logf("failed to read data from connection: %s", derr) break } + if props.EchoBuffer != nil { + if _, err = props.EchoBuffer.Write([]byte(ddata)); err != nil { + t.Errorf("failed write to echo buffer: %s", err) + } + } ddata = strings.TrimSpace(ddata) if ddata == "." { if props.FailOnDataClose { From 5d977e7206f62f39d5f2a798335f6c264572950e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:21:05 +0000 Subject: [PATCH 038/125] Bump github/codeql-action from 3.27.0 to 3.27.1 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.0 to 3.27.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/662472033e021d55d94146f66f6058822b0b39fd...4f3212b61783c3c68e8309a0f18a699764811cda) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 01b02ec..195e5e8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/autobuild@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -79,4 +79,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 0d5ccfd..61f7553 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 with: sarif_file: results.sarif From 87accd289ef855ca876d784ea8b68d5c9326346d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 16:41:44 +0100 Subject: [PATCH 039/125] Fix formatting in smtp_test.go and add new test cases Corrected indentation inconsistencies in the smtp_test.go file. Added multiple test cases to verify the failure scenarios for `SendMail` under various conditions including invalid hostnames, newlines in the address fields, and server failures during EHLO/HELO, STARTTLS, and authentication stages. --- smtp/smtp_test.go | 265 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 253 insertions(+), 12 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index e7649f8..b09eda6 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -379,7 +379,8 @@ func TestPlainAuth(t *testing.T) { go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -411,7 +412,8 @@ func TestPlainAuth(t *testing.T) { if err := simpleSMTPServer(ctx, t, &serverProps{ FailOnAuth: true, FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -532,7 +534,8 @@ func TestPlainAuth_noEnc(t *testing.T) { go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -564,7 +567,8 @@ func TestPlainAuth_noEnc(t *testing.T) { if err := simpleSMTPServer(ctx, t, &serverProps{ FailOnAuth: true, FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -681,7 +685,8 @@ func TestLoginAuth(t *testing.T) { go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -713,7 +718,8 @@ func TestLoginAuth(t *testing.T) { if err := simpleSMTPServer(ctx, t, &serverProps{ FailOnAuth: true, FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -827,7 +833,8 @@ func TestLoginAuth_noEnc(t *testing.T) { go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -859,7 +866,8 @@ func TestLoginAuth_noEnc(t *testing.T) { if err := simpleSMTPServer(ctx, t, &serverProps{ FailOnAuth: true, FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -996,7 +1004,8 @@ func TestXOAuth2Auth(t *testing.T) { go func() { if err := simpleSMTPServer(ctx, t, &serverProps{ FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -1028,7 +1037,8 @@ func TestXOAuth2Auth(t *testing.T) { if err := simpleSMTPServer(ctx, t, &serverProps{ FailOnAuth: true, FeatureSet: featureSet, - ListenPort: serverPort}, + ListenPort: serverPort, + }, ); err != nil { t.Errorf("failed to start test server: %s", err) return @@ -2544,6 +2554,34 @@ func TestClient_Data(t *testing.T) { func TestSendMail(t *testing.T) { t.Run("full SendMail transaction with TLS and auth", func(t *testing.T) { + want := []string{ + "220 go-mail test server ready ESMTP", + "EHLO localhost", + "250-localhost.localdomain", + "250-AUTH LOGIN", + "250-DSN", + "250 STARTTLS", + "STARTTLS", + "220 Ready to start TLS", + "EHLO localhost", + "250-localhost.localdomain", + "250-AUTH LOGIN", + "250-DSN", + "250 STARTTLS", + "AUTH LOGIN", + "235 2.7.0 Authentication successful", + "MAIL FROM:", + "250 2.0.0 OK", + "RCPT TO:", + "250 2.0.0 OK", + "DATA", + "354 End data with .", + "test message", + ".", + "250 2.0.0 Ok: queued as 1234567890", + "QUIT", + "221 2.0.0 Bye", + } ctx, cancel := context.WithCancel(context.Background()) defer cancel() PortAdder.Add(1) @@ -2575,8 +2613,211 @@ func TestSendMail(t *testing.T) { t.Fatalf("failed to send mail: %s", err) } resp := strings.Split(echoBuffer.String(), "\r\n") - for i, line := range resp { - t.Logf("response line %d: %q", i, line) + if len(resp)-1 != len(want) { + t.Fatalf("expected %d lines, but got %d", len(want), len(resp)) + } + for i := 0; i < len(want); i++ { + if !strings.EqualFold(resp[i], want[i]) { + t.Errorf("expected line %d to be %q, but got %q", i, resp[i], want[i]) + } + } + }) + t.Run("SendMail newline in from should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld\r\n", []string{"valid-to@domain.tld"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail with newlines in from address") + } + }) + t.Run("SendMail newline in to should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld\r\n"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail with newlines in to address") + } + }) + t.Run("SendMail with invalid hostname should fail", func(t *testing.T) { + addr := "invalid.invalid-hostname.tld:1234" + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail with invalid server address") + } + }) + t.Run("SendMail should fail on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail on EHLO/HELO") + } + }) + t.Run("SendMail should fail on STARTTLS", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + testHookStartTLS = func(config *tls.Config) { + config.ServerName = "invalid.invalid-hostname.tld" + config.RootCAs = nil + config.Certificates = nil + } + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail on STARTTLS") + } + }) + t.Run("SendMail should fail on no auth support", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail on no auth support") + } + }) + t.Run("SendMail should fail on auth", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnAuth: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) + testHookStartTLS = func(config *tls.Config) { + testConfig := getTLSConfig(t) + config.ServerName = testConfig.ServerName + config.RootCAs = testConfig.RootCAs + config.Certificates = testConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, + []byte("test message")); err == nil { + t.Error("expected SendMail to fail on auth") } }) } From 1bfec504ed38d6ca6b3274b8ff5aa5267a0c37fa Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 17:12:15 +0100 Subject: [PATCH 040/125] Add new SMTP test cases for various failure scenarios This commit introduces multiple test cases to handle SMTP failure scenarios such as invalid host, newline in addresses, EHLO/HELO failures, and malformed data injections. Additionally, it includes improvements in error handling for these specific conditions. --- smtp/smtp_test.go | 576 ++++++++++++++++++++-------------------------- 1 file changed, 246 insertions(+), 330 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index b09eda6..c54b868 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -2295,6 +2295,44 @@ func TestClient_Mail(t *testing.T) { t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } }) + t.Run("from address and server supports SMTPUTF8 with unicode address", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-SMTPUTF8\r\n250 STARTTLS" + echoBuffer := bytes.NewBuffer(nil) + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Mail("valid-from+📧@domain.tld"); err != nil { + t.Errorf("failed to set mail from address: %s", err) + } + expected := "MAIL FROM: SMTPUTF8" + resp := strings.Split(echoBuffer.String(), "\r\n") + if !strings.EqualFold(resp[5], expected) { + t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) + } + }) t.Run("from address and server supports DSN", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -2325,8 +2363,8 @@ func TestClient_Mail(t *testing.T) { } }) client.dsnmrtype = "FULL" - if err = client.Mail("valid-from@domain.tld"); err == nil { - t.Error("server should echo the command as error but didn't") + if err = client.Mail("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: RET=FULL" resp := strings.Split(echoBuffer.String(), "\r\n") @@ -2364,8 +2402,8 @@ func TestClient_Mail(t *testing.T) { } }) client.dsnmrtype = "FULL" - if err = client.Mail("valid-from@domain.tld"); err == nil { - t.Error("server should echo the command as error but didn't") + if err = client.Mail("valid-from@domain.tld"); err != nil { + t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: BODY=8BITMIME SMTPUTF8 RET=FULL" resp := strings.Split(echoBuffer.String(), "\r\n") @@ -2553,6 +2591,153 @@ func TestClient_Data(t *testing.T) { } func TestSendMail(t *testing.T) { + tests := []struct { + name string + featureSet string + hostname string + tlsConfig *tls.Config + props *serverProps + fromAddr string + toAddr string + message []byte + }{ + { + "fail on newline in MAIL FROM address", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{}, + "valid-from@domain.tld\r\n", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on newline in RCPT TO address", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{}, + "valid-from@domain.tld", + "valid-to@domain.tld\r\n", + []byte("test message"), + }, + { + "fail on invalid host address", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + "invalid.invalid-host@domain.tld", + getTLSConfig(t), + &serverProps{}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on EHLO/HELO", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{FailOnEhlo: true, FailOnHelo: true}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on STARTTLS", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + &tls.Config{ServerName: "invalid.invalid-host@domain.tld"}, + &serverProps{}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on no server AUTH support", + "250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on AUTH", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{FailOnAuth: true}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on MAIL FROM", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{FailOnMailFrom: true}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on RCPT TO", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{FailOnRcptTo: true}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on DATA (init phase)", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{FailOnDataInit: true}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + { + "fail on DATA (closing phase)", + "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS", + TestServerAddr, + getTLSConfig(t), + &serverProps{FailOnDataClose: true}, + "valid-from@domain.tld", + "valid-to@domain.tld", + []byte("test message"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + tt.props.ListenPort = int(TestServerPortBase + PortAdder.Load()) + tt.props.FeatureSet = tt.featureSet + go func() { + if err := simpleSMTPServer(ctx, t, tt.props); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := fmt.Sprintf("%s:%d", tt.hostname, tt.props.ListenPort) + testHookStartTLS = func(config *tls.Config) { + config.ServerName = tt.tlsConfig.ServerName + config.RootCAs = tt.tlsConfig.RootCAs + config.Certificates = tt.tlsConfig.Certificates + } + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, tt.fromAddr, []string{tt.toAddr}, tt.message); err == nil { + t.Error("expected SendMail to " + tt.name) + } + }) + } t.Run("full SendMail transaction with TLS and auth", func(t *testing.T) { want := []string{ "220 go-mail test server ready ESMTP", @@ -2622,37 +2807,41 @@ func TestSendMail(t *testing.T) { } } }) - t.Run("SendMail newline in from should fail", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - PortAdder.Add(1) - serverPort := int(TestServerPortBase + PortAdder.Load()) - featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" - go func() { - if err := simpleSMTPServer(ctx, t, &serverProps{ - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 30) - addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) - testHookStartTLS = func(config *tls.Config) { - testConfig := getTLSConfig(t) - config.ServerName = testConfig.ServerName - config.RootCAs = testConfig.RootCAs - config.Certificates = testConfig.Certificates + t.Run("full SendMail transaction with leading dots", func(t *testing.T) { + want := []string{ + "220 go-mail test server ready ESMTP", + "EHLO localhost", + "250-localhost.localdomain", + "250-AUTH LOGIN", + "250-DSN", + "250 STARTTLS", + "STARTTLS", + "220 Ready to start TLS", + "EHLO localhost", + "250-localhost.localdomain", + "250-AUTH LOGIN", + "250-DSN", + "250 STARTTLS", + "AUTH LOGIN", + "235 2.7.0 Authentication successful", + "MAIL FROM:", + "250 2.0.0 OK", + "RCPT TO:", + "250 2.0.0 OK", + "DATA", + "354 End data with .", + "From: user@gmail.com", + "To: golang-nuts@googlegroups.com", + "Subject: Hooray for Go", + "", + "Line 1", + "..Leading dot line .", + "Goodbye.", + ".", + "250 2.0.0 Ok: queued as 1234567890", + "QUIT", + "221 2.0.0 Bye", } - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld\r\n", []string{"valid-to@domain.tld"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail with newlines in from address") - } - }) - t.Run("SendMail newline in to should fail", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() PortAdder.Add(1) @@ -2678,309 +2867,31 @@ func TestSendMail(t *testing.T) { config.RootCAs = testConfig.RootCAs config.Certificates = testConfig.Certificates } - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld\r\n"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail with newlines in to address") - } - }) - t.Run("SendMail with invalid hostname should fail", func(t *testing.T) { - addr := "invalid.invalid-hostname.tld:1234" - testHookStartTLS = func(config *tls.Config) { - testConfig := getTLSConfig(t) - config.ServerName = testConfig.ServerName - config.RootCAs = testConfig.RootCAs - config.Certificates = testConfig.Certificates - } - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail with invalid server address") - } - }) - t.Run("SendMail should fail on EHLO/HELO", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - PortAdder.Add(1) - serverPort := int(TestServerPortBase + PortAdder.Load()) - featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" - go func() { - if err := simpleSMTPServer(ctx, t, &serverProps{ - FailOnEhlo: true, - FailOnHelo: true, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 30) - addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) - testHookStartTLS = func(config *tls.Config) { - testConfig := getTLSConfig(t) - config.ServerName = testConfig.ServerName - config.RootCAs = testConfig.RootCAs - config.Certificates = testConfig.Certificates - } - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail on EHLO/HELO") - } - }) - t.Run("SendMail should fail on STARTTLS", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - PortAdder.Add(1) - serverPort := int(TestServerPortBase + PortAdder.Load()) - featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" - go func() { - if err := simpleSMTPServer(ctx, t, &serverProps{ - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - testHookStartTLS = func(config *tls.Config) { - config.ServerName = "invalid.invalid-hostname.tld" - config.RootCAs = nil - config.Certificates = nil - } - time.Sleep(time.Millisecond * 30) - addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail on STARTTLS") - } - }) - t.Run("SendMail should fail on no auth support", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - PortAdder.Add(1) - serverPort := int(TestServerPortBase + PortAdder.Load()) - featureSet := "250-DSN\r\n250 STARTTLS" - go func() { - if err := simpleSMTPServer(ctx, t, &serverProps{ - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 30) - addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) - testHookStartTLS = func(config *tls.Config) { - testConfig := getTLSConfig(t) - config.ServerName = testConfig.ServerName - config.RootCAs = testConfig.RootCAs - config.Certificates = testConfig.Certificates - } - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail on no auth support") - } - }) - t.Run("SendMail should fail on auth", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - PortAdder.Add(1) - serverPort := int(TestServerPortBase + PortAdder.Load()) - featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" - go func() { - if err := simpleSMTPServer(ctx, t, &serverProps{ - FailOnAuth: true, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { - t.Errorf("failed to start test server: %s", err) - return - } - }() - time.Sleep(time.Millisecond * 30) - addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) - testHookStartTLS = func(config *tls.Config) { - testConfig := getTLSConfig(t) - config.ServerName = testConfig.ServerName - config.RootCAs = testConfig.RootCAs - config.Certificates = testConfig.Certificates - } - auth := LoginAuth("username", "password", TestServerAddr, false) - if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, - []byte("test message")); err == nil { - t.Error("expected SendMail to fail on auth") - } - }) -} - -/* -func TestBasic(t *testing.T) { - server := strings.Join(strings.Split(basicServer, "\n"), "\r\n") - client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") - - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c := &Client{Text: textproto.NewConn(fake), localName: "localhost"} - - if err := c.helo(); err != nil { - t.Fatalf("HELO failed: %s", err) - } - if err := c.ehlo(); err == nil { - t.Fatalf("Expected first EHLO to fail") - } - if err := c.ehlo(); err != nil { - t.Fatalf("Second EHLO failed: %s", err) - } - - c.didHello = true - if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { - t.Fatalf("Expected AUTH supported") - } - if ok, _ := c.Extension("DSN"); ok { - t.Fatalf("Shouldn't support DSN") - } - - if err := c.Mail("user@gmail.com"); err == nil { - t.Fatalf("MAIL should require authentication") - } - - if err := c.Verify("user1@gmail.com"); err == nil { - t.Fatalf("First VRFY: expected no verification") - } - if err := c.Verify("user2@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil { - t.Fatalf("VRFY should have failed due to a message injection attempt") - } - if err := c.Verify("user2@gmail.com"); err != nil { - t.Fatalf("Second VRFY: expected verification, got %s", err) - } - - // fake TLS so authentication won't complain - c.tls = true - c.serverName = "smtp.google.com" - if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false)); err != nil { - t.Fatalf("AUTH failed: %s", err) - } - - if err := c.Rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil { - t.Fatalf("RCPT should have failed due to a message injection attempt") - } - if err := c.Mail("user@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil { - t.Fatalf("MAIL should have failed due to a message injection attempt") - } - if err := c.Mail("user@gmail.com"); err != nil { - t.Fatalf("MAIL failed: %s", err) - } - if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { - t.Fatalf("RCPT failed: %s", err) - } - msg := `From: user@gmail.com + message := []byte(`From: user@gmail.com To: golang-nuts@googlegroups.com Subject: Hooray for Go Line 1 .Leading dot line . -Goodbye.` - w, err := c.Data() - if err != nil { - t.Fatalf("DATA failed: %s", err) - } - if _, err := w.Write([]byte(msg)); err != nil { - t.Fatalf("Data write failed: %s", err) - } - if err := w.Close(); err != nil { - t.Fatalf("Bad data response: %s", err) - } - - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } +Goodbye.`) + auth := LoginAuth("username", "password", TestServerAddr, false) + if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, message); err != nil { + t.Fatalf("failed to send mail: %s", err) + } + resp := strings.Split(echoBuffer.String(), "\r\n") + if len(resp)-1 != len(want) { + t.Errorf("expected %d lines, but got %d", len(want), len(resp)) + } + for i := 0; i < len(want); i++ { + if !strings.EqualFold(resp[i], want[i]) { + t.Errorf("expected line %d to be %q, but got %q", i, resp[i], want[i]) + } + } + }) } -var basicServer = `250 mx.google.com at your service -502 Unrecognized command. -250-mx.google.com at your service -250-SIZE 35651584 -250-AUTH LOGIN PLAIN -250 8BITMIME -530 Authentication required -252 Send some mail, I'll try my best -250 User is valid -235 Accepted -250 Sender OK -250 Receiver OK -354 Go ahead -250 Data OK -221 OK -` +/* -var basicClient = `HELO localhost -EHLO localhost -EHLO localhost -MAIL FROM: BODY=8BITMIME -VRFY user1@gmail.com -VRFY user2@gmail.com -AUTH PLAIN AHVzZXIAcGFzcw== -MAIL FROM: BODY=8BITMIME -RCPT TO: -DATA -From: user@gmail.com -To: golang-nuts@googlegroups.com -Subject: Hooray for Go - -Line 1 -..Leading dot line . -Goodbye. -. -QUIT -` - -func TestHELOFailed(t *testing.T) { - serverLines := `502 EH? -502 EH? -221 OK -` - clientLines := `EHLO localhost -HELO localhost -QUIT -` - server := strings.Join(strings.Split(serverLines, "\n"), "\r\n") - client := strings.Join(strings.Split(clientLines, "\n"), "\r\n") - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c := &Client{Text: textproto.NewConn(fake), localName: "localhost"} - if err := c.Hello("localhost"); err == nil { - t.Fatal("expected EHLO to fail") - } - if err := c.Quit(); err != nil { - t.Errorf("QUIT failed: %s", err) - } - _ = bcmdbuf.Flush() - actual := cmdbuf.String() - if client != actual { - t.Errorf("Got:\n%s\nWant:\n%s", actual, client) - } -} func TestExtensions(t *testing.T) { fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) { @@ -4351,6 +4262,7 @@ type serverProps struct { FailOnNoop bool FailOnQuit bool FailOnReset bool + FailOnRcptTo bool FailOnSTARTTLS bool FailTemp bool FeatureSet string @@ -4502,12 +4414,16 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server from = strings.ReplaceAll(from, "RET=FULL", "") } from = strings.TrimSpace(from) - if !strings.EqualFold(from, "") { + if !strings.HasPrefix(from, "") { writeLine(fmt.Sprintf("503 5.1.2 Invalid from: %s", from)) break } writeOK() case strings.HasPrefix(data, "RCPT TO:"): + if props.FailOnRcptTo { + writeLine("500 5.5.2 Error: fail on RCPT TO") + break + } to := strings.TrimPrefix(data, "RCPT TO:") if props.SupportDSN { to = strings.ReplaceAll(to, "NOTIFY=FAILURE,SUCCESS", "") From 2d384a7d3765878001d9dbf4fc86267f4014738f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 17:29:43 +0100 Subject: [PATCH 041/125] Add unit tests for SMTP client extensions, reset, and noop Introduced new unit tests to verify the SMTP client's behavior with extensions, reset, and noop commands under various server conditions. Updated server response handling to correctly manage feature sets when they are empty. --- smtp/smtp_test.go | 239 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 2 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index c54b868..401a6ac 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -2890,6 +2890,233 @@ Goodbye.`) }) } +func TestClient_Extension(t *testing.T) { + t.Run("extension check fails on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if ok, _ := client.Extension("DSN"); ok { + t.Error("expected client extension check to fail on EHLO/HELO") + } + }) +} + +func TestClient_Reset(t *testing.T) { + t.Run("reset on functioning client conneciton", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Reset(); err != nil { + t.Errorf("failed to reset client: %s", err) + } + }) + t.Run("reset fails on RSET", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnReset: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Reset(); err == nil { + t.Error("expected client reset to fail") + } + }) + t.Run("reset fails on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Reset(); err == nil { + t.Error("expected client reset to fail") + } + }) +} + +func TestClient_Noop(t *testing.T) { + t.Run("noop on functioning client conneciton", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Noop(); err != nil { + t.Errorf("failed client no-operation: %s", err) + } + }) + t.Run("noop fails on EHLO/HELO", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnEhlo: true, + FailOnHelo: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Noop(); err == nil { + t.Error("expected client no-operation to fail") + } + }) + t.Run("noop fails on NOOP", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnNoop: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.Noop(); err == nil { + t.Error("expected client no-operation to fail") + } + }) +} + /* @@ -4391,7 +4618,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server writeLine("500 5.5.2 Error: fail on HELO") break } - writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + if props.FeatureSet != "" { + writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + break + } + writeLine("250 localhost.localdomain\r\n") case strings.HasPrefix(data, "EHLO"): if len(strings.Split(data, " ")) != 2 { writeLine("501 Syntax: EHLO hostname") @@ -4401,7 +4632,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server writeLine("500 5.5.2 Error: fail on EHLO") break } - writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + if props.FeatureSet != "" { + writeLine("250-localhost.localdomain\r\n" + props.FeatureSet) + break + } + writeLine("250 localhost.localdomain\r\n") case strings.HasPrefix(data, "MAIL FROM:"): if props.FailOnMailFrom { writeLine("500 5.5.2 Error: fail on MAIL FROM") From 2cc670659eb16857018560b3d624b31c880fb14f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 17:36:24 +0100 Subject: [PATCH 042/125] Remove obsolete SMTP client tests Deleted multiple outdated and redundant tests from the SMTP client test suite to streamline and improve the maintainability of the test codebase. This change focuses on removing tests that are no longer relevant or have been superseded by other methods. --- smtp/smtp_test.go | 1337 --------------------------------------------- 1 file changed, 1337 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 401a6ac..11cc2a0 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3117,1343 +3117,6 @@ func TestClient_Noop(t *testing.T) { }) } -/* - - -func TestExtensions(t *testing.T) { - fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) { - server = strings.Join(strings.Split(server, "\n"), "\r\n") - - cmdbuf = &strings.Builder{} - bcmdbuf = bufio.NewWriter(cmdbuf) - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c = &Client{Text: textproto.NewConn(fake), localName: "localhost"} - - return c, bcmdbuf, cmdbuf - } - - t.Run("helo", func(t *testing.T) { - const ( - basicServer = `250 mx.google.com at your service -250 Sender OK -221 Goodbye -` - - basicClient = `HELO localhost -MAIL FROM: -QUIT -` - ) - - c, bcmdbuf, cmdbuf := fake(basicServer) - - if err := c.helo(); err != nil { - t.Fatalf("HELO failed: %s", err) - } - c.didHello = true - if err := c.Mail("user@gmail.com"); err != nil { - t.Fatalf("MAIL FROM failed: %s", err) - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } - }) - - t.Run("ehlo", func(t *testing.T) { - const ( - basicServer = `250-mx.google.com at your service -250 SIZE 35651584 -250 Sender OK -221 Goodbye -` - - basicClient = `EHLO localhost -MAIL FROM: -QUIT -` - ) - - c, bcmdbuf, cmdbuf := fake(basicServer) - - if err := c.Hello("localhost"); err != nil { - t.Fatalf("EHLO failed: %s", err) - } - if ok, _ := c.Extension("8BITMIME"); ok { - t.Fatalf("Shouldn't support 8BITMIME") - } - if ok, _ := c.Extension("SMTPUTF8"); ok { - t.Fatalf("Shouldn't support SMTPUTF8") - } - if err := c.Mail("user@gmail.com"); err != nil { - t.Fatalf("MAIL FROM failed: %s", err) - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } - }) - - t.Run("ehlo 8bitmime", func(t *testing.T) { - const ( - basicServer = `250-mx.google.com at your service -250-SIZE 35651584 -250 8BITMIME -250 Sender OK -221 Goodbye -` - - basicClient = `EHLO localhost -MAIL FROM: BODY=8BITMIME -QUIT -` - ) - - c, bcmdbuf, cmdbuf := fake(basicServer) - - if err := c.Hello("localhost"); err != nil { - t.Fatalf("EHLO failed: %s", err) - } - if ok, _ := c.Extension("8BITMIME"); !ok { - t.Fatalf("Should support 8BITMIME") - } - if ok, _ := c.Extension("SMTPUTF8"); ok { - t.Fatalf("Shouldn't support SMTPUTF8") - } - if err := c.Mail("user@gmail.com"); err != nil { - t.Fatalf("MAIL FROM failed: %s", err) - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - actualcmds := cmdbuf.String() - client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } - }) - - t.Run("ehlo smtputf8", func(t *testing.T) { - const ( - basicServer = `250-mx.google.com at your service -250-SIZE 35651584 -250 SMTPUTF8 -250 Sender OK -221 Goodbye -` - - basicClient = `EHLO localhost -MAIL FROM: SMTPUTF8 -QUIT -` - ) - - c, bcmdbuf, cmdbuf := fake(basicServer) - - if err := c.Hello("localhost"); err != nil { - t.Fatalf("EHLO failed: %s", err) - } - if ok, _ := c.Extension("8BITMIME"); ok { - t.Fatalf("Shouldn't support 8BITMIME") - } - if ok, _ := c.Extension("SMTPUTF8"); !ok { - t.Fatalf("Should support SMTPUTF8") - } - if err := c.Mail("user+📧@gmail.com"); err != nil { - t.Fatalf("MAIL FROM failed: %s", err) - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - actualcmds := cmdbuf.String() - client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } - }) - - t.Run("ehlo 8bitmime smtputf8", func(t *testing.T) { - const ( - basicServer = `250-mx.google.com at your service -250-SIZE 35651584 -250-8BITMIME -250 SMTPUTF8 -250 Sender OK -221 Goodbye - ` - - basicClient = `EHLO localhost -MAIL FROM: BODY=8BITMIME SMTPUTF8 -QUIT -` - ) - - c, bcmdbuf, cmdbuf := fake(basicServer) - - if err := c.Hello("localhost"); err != nil { - t.Fatalf("EHLO failed: %s", err) - } - c.didHello = true - if ok, _ := c.Extension("8BITMIME"); !ok { - t.Fatalf("Should support 8BITMIME") - } - if ok, _ := c.Extension("SMTPUTF8"); !ok { - t.Fatalf("Should support SMTPUTF8") - } - if err := c.Mail("user+📧@gmail.com"); err != nil { - t.Fatalf("MAIL FROM failed: %s", err) - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - actualcmds := cmdbuf.String() - client := strings.Join(strings.Split(basicClient, "\n"), "\r\n") - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } - }) -} - -func TestNewClient(t *testing.T) { - server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") - client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n") - - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - out := func() string { - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - return cmdbuf.String() - } - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v\n(after %v)", err, out()) - } - defer func() { - _ = c.Close() - }() - if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" { - t.Fatalf("Expected AUTH supported") - } - if ok, _ := c.Extension("DSN"); ok { - t.Fatalf("Shouldn't support DSN") - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - actualcmds := out() - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } -} - -// TestClient_SetDebugLog tests the Client method with the Client.SetDebugLog method -// to enable debug logging -func TestClient_SetDebugLog(t *testing.T) { - server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") - - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - out := func() string { - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - return cmdbuf.String() - } - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v\n(after %v)", err, out()) - } - defer func() { - _ = c.Close() - }() - c.SetDebugLog(true) - if !c.debug { - t.Errorf("Expected DebugLog flag to be true but received false") - } -} - -// TestClient_SetLogger tests the Client method with the Client.SetLogger method -// to provide a custom logger -func TestClient_SetLogger(t *testing.T) { - server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") - - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - out := func() string { - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - return cmdbuf.String() - } - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v\n(after %v)", err, out()) - } - defer func() { - _ = c.Close() - }() - c.SetLogger(log.New(os.Stderr, log.LevelDebug)) - if c.logger == nil { - t.Errorf("Expected Logger to be set but received nil") - } - c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}}) - c.SetLogger(nil) - c.logger.Debugf(log.Log{Direction: log.DirServerToClient, Format: "%s", Messages: []interface{}{"test"}}) -} - -func TestClient_SetLogAuthData(t *testing.T) { - server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") - - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - out := func() string { - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("failed to flush: %s", err) - } - return cmdbuf.String() - } - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v\n(after %v)", err, out()) - } - defer func() { - _ = c.Close() - }() - c.SetLogAuthData() - if !c.logAuthData { - t.Error("Expected logAuthData to be true but received false") - } -} - -var newClientServer = `220 hello world -250-mx.google.com at your service -250-SIZE 35651584 -250-AUTH LOGIN PLAIN -250 8BITMIME -221 OK -` - -var newClientClient = `EHLO localhost -QUIT -` - -func TestNewClient2(t *testing.T) { - server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n") - client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n") - - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v", err) - } - defer func() { - _ = c.Close() - }() - if ok, _ := c.Extension("DSN"); ok { - t.Fatalf("Shouldn't support DSN") - } - if err := c.Quit(); err != nil { - t.Fatalf("QUIT failed: %s", err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - if client != actualcmds { - t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } -} - -var newClient2Server = `220 hello world -502 EH? -250-mx.google.com at your service -250-SIZE 35651584 -250-AUTH LOGIN PLAIN -250 8BITMIME -221 OK -` - -var newClient2Client = `EHLO localhost -HELO localhost -QUIT -` - -func TestNewClientWithTLS(t *testing.T) { - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - t.Fatalf("loadcert: %v", err) - } - - config := tls.Config{Certificates: []tls.Certificate{cert}} - - ln, err := tls.Listen("tcp", "127.0.0.1:0", &config) - if err != nil { - ln, err = tls.Listen("tcp", "[::1]:0", &config) - if err != nil { - t.Fatalf("server: listen: %v", err) - } - } - - go func() { - conn, err := ln.Accept() - if err != nil { - t.Errorf("server: accept: %v", err) - return - } - defer func() { - _ = conn.Close() - }() - - _, err = conn.Write([]byte("220 SIGNS\r\n")) - if err != nil { - t.Errorf("server: write: %v", err) - return - } - }() - - config.InsecureSkipVerify = true - conn, err := tls.Dial("tcp", ln.Addr().String(), &config) - if err != nil { - t.Fatalf("client: dial: %v", err) - } - defer func() { - _ = conn.Close() - }() - - client, err := NewClient(conn, ln.Addr().String()) - if err != nil { - t.Fatalf("smtp: newclient: %v", err) - } - if !client.tls { - t.Errorf("client.tls Got: %t Expected: %t", client.tls, true) - } -} - -func TestHello(t *testing.T) { - if len(helloServer) != len(helloClient) { - t.Fatalf("Hello server and client size mismatch") - } - - tf := func(fake faker, i int) error { - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v", err) - } - defer func() { - _ = c.Close() - }() - c.localName = "customhost" - err = nil - - switch i { - case 0: - err = c.Hello("hostinjection>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n") - if err == nil { - t.Errorf("Expected Hello to be rejected due to a message injection attempt") - } - err = c.Hello("customhost") - case 1: - err = c.StartTLS(nil) - if err.Error() == "502 Not implemented" { - err = nil - } - case 2: - err = c.Verify("test@example.com") - case 3: - c.tls = true - c.serverName = "smtp.google.com" - err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false)) - case 4: - err = c.Mail("test@example.com") - case 5: - ok, _ := c.Extension("feature") - if ok { - t.Errorf("Expected FEATURE not to be supported") - } - case 6: - err = c.Reset() - case 7: - err = c.Quit() - case 8: - err = c.Verify("test@example.com") - if err != nil { - err = c.Hello("customhost") - if err != nil { - t.Errorf("Want error, got none") - } - } - case 9: - err = c.Noop() - default: - t.Fatalf("Unhandled command") - } - - if err != nil { - t.Errorf("Command %d failed: %v", i, err) - } - return nil - } - - for i := 0; i < len(helloServer); i++ { - server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n") - client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n") - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - - if err := tf(fake, i); err != nil { - t.Error(err) - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - if client != actualcmds { - t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } - } -} - -var baseHelloServer = `220 hello world -502 EH? -250-mx.google.com at your service -250 FEATURE -` - -var helloServer = []string{ - "", - "502 Not implemented\n", - "250 User is valid\n", - "235 Accepted\n", - "250 Sender ok\n", - "", - "250 Reset ok\n", - "221 Goodbye\n", - "250 Sender ok\n", - "250 ok\n", -} - -var baseHelloClient = `EHLO customhost -HELO customhost -` - -var helloClient = []string{ - "", - "STARTTLS\n", - "VRFY test@example.com\n", - "AUTH PLAIN AHVzZXIAcGFzcw==\n", - "MAIL FROM:\n", - "", - "RSET\n", - "QUIT\n", - "VRFY test@example.com\n", - "NOOP\n", -} - -func TestSendMail(t *testing.T) { - server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n") - client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n") - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Unable to create listener: %v", err) - } - defer func() { - _ = l.Close() - }() - - // prevent data race on bcmdbuf - done := make(chan struct{}) - go func(data []string) { - defer close(done) - - conn, err := l.Accept() - if err != nil { - t.Errorf("Accept error: %v", err) - return - } - defer func() { - _ = conn.Close() - }() - - tc := textproto.NewConn(conn) - for i := 0; i < len(data) && data[i] != ""; i++ { - if err := tc.PrintfLine("%s", data[i]); err != nil { - t.Errorf("printing to textproto failed: %s", err) - } - for len(data[i]) >= 4 && data[i][3] == '-' { - i++ - if err := tc.PrintfLine("%s", data[i]); err != nil { - t.Errorf("printing to textproto failed: %s", err) - } - } - if data[i] == "221 Goodbye" { - return - } - read := false - for !read || data[i] == "354 Go ahead" { - msg, err := tc.ReadLine() - if _, err := bcmdbuf.Write([]byte(msg + "\r\n")); err != nil { - t.Errorf("write failed: %s", err) - } - read = true - if err != nil { - t.Errorf("Read error: %v", err) - return - } - if data[i] == "354 Go ahead" && msg == "." { - break - } - } - } - }(strings.Split(server, "\r\n")) - - err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}, []byte(strings.Replace(`From: test@example.com -To: other@example.com -Subject: SendMail test - -SendMail is working for me. -`, "\n", "\r\n", -1))) - if err == nil { - t.Errorf("Expected SendMail to be rejected due to a message injection attempt") - } - - err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com -To: other@example.com -Subject: SendMail test - -SendMail is working for me. -`, "\n", "\r\n", -1))) - if err != nil { - t.Errorf("%v", err) - } - - <-done - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - if client != actualcmds { - t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } -} - -var sendMailServer = `220 hello world -502 EH? -250 mx.google.com at your service -250 Sender ok -250 Receiver ok -354 Go ahead -250 Data ok -221 Goodbye -` - -var sendMailClient = `EHLO localhost -HELO localhost -MAIL FROM: -RCPT TO: -DATA -From: test@example.com -To: other@example.com -Subject: SendMail test - -SendMail is working for me. -. -QUIT -` - -func TestSendMailWithAuth(t *testing.T) { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("Unable to create listener: %v", err) - } - defer func() { - _ = l.Close() - }() - - errCh := make(chan error) - go func() { - defer close(errCh) - conn, err := l.Accept() - if err != nil { - errCh <- fmt.Errorf("listener Accept: %w", err) - return - } - defer func() { - _ = conn.Close() - }() - - tc := textproto.NewConn(conn) - if err := tc.PrintfLine("220 hello world"); err != nil { - t.Errorf("textproto connetion print failed: %s", err) - } - msg, err := tc.ReadLine() - if err != nil { - errCh <- fmt.Errorf("textproto connection ReadLine error: %w", err) - return - } - const wantMsg = "EHLO localhost" - if msg != wantMsg { - errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg) - return - } - err = tc.PrintfLine("250 mx.google.com at your service") - if err != nil { - errCh <- fmt.Errorf("textproto connection PrintfLine: %w", err) - return - } - }() - - err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com", false), "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com -To: other@example.com -Subject: SendMail test - -SendMail is working for me. -`, "\n", "\r\n", -1))) - if err == nil { - t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ") - return - } - if err.Error() != "smtp: server doesn't support AUTH" { - t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err) - } - err = <-errCh - if err != nil { - t.Fatalf("server error: %v", err) - } -} - -func TestAuthFailed(t *testing.T) { - server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n") - client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n") - var cmdbuf strings.Builder - bcmdbuf := bufio.NewWriter(&cmdbuf) - var fake faker - fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) - c, err := NewClient(fake, "fake.host") - if err != nil { - t.Fatalf("NewClient: %v", err) - } - defer func() { - _ = c.Close() - }() - - c.tls = true - c.serverName = "smtp.google.com" - err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com", false)) - - if err == nil { - t.Error("Auth: expected error; got none") - } else if err.Error() != "535 Invalid credentials\nplease see www.example.com" { - t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com") - } - - if err := bcmdbuf.Flush(); err != nil { - t.Errorf("flush failed: %s", err) - } - actualcmds := cmdbuf.String() - if client != actualcmds { - t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) - } -} - -var authFailedServer = `220 hello world -250-mx.google.com at your service -250 AUTH LOGIN PLAIN -535-Invalid credentials -535 please see www.example.com -221 Goodbye -` - -var authFailedClient = `EHLO localhost -AUTH PLAIN AHVzZXIAcGFzcw== -* -QUIT -` - -func TestTLSClient(t *testing.T) { - if runtime.GOOS == "freebsd" || runtime.GOOS == "js" || runtime.GOOS == "wasip1" { - SkipFlaky(t, 19229) - } - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - errc := make(chan error) - go func() { - errc <- sendMail(ln.Addr().String()) - }() - conn, err := ln.Accept() - if err != nil { - t.Fatalf("failed to accept connection: %v", err) - } - defer func() { - _ = conn.Close() - }() - if err := serverHandle(conn, t); err != nil { - t.Fatalf("failed to handle connection: %v", err) - } - if err := <-errc; err != nil { - t.Fatalf("client error: %v", err) - } -} - -func TestTLSConnState(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Quit() - }() - cfg := &tls.Config{ServerName: "example.com"} - testHookStartTLS(cfg) // set the RootCAs - if err := c.StartTLS(cfg); err != nil { - t.Errorf("StartTLS: %v", err) - return - } - cs, ok := c.TLSConnectionState() - if !ok { - t.Errorf("TLSConnectionState returned ok == false; want true") - return - } - if cs.Version == 0 || !cs.HandshakeComplete { - t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs) - } - }() - <-clientDone - <-serverDone -} - -func TestClient_GetTLSConnectionState(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Quit() - }() - cfg := &tls.Config{ServerName: "example.com"} - testHookStartTLS(cfg) // set the RootCAs - if err := c.StartTLS(cfg); err != nil { - t.Errorf("StartTLS: %v", err) - return - } - cs, err := c.GetTLSConnectionState() - if err != nil { - t.Errorf("failed to get TLSConnectionState: %s", err) - return - } - if cs.Version == 0 || !cs.HandshakeComplete { - t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs) - } - }() - <-clientDone - <-serverDone -} - -func TestClient_GetTLSConnectionState_noTLS(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Quit() - }() - _, err = c.GetTLSConnectionState() - if err == nil { - t.Error("GetTLSConnectionState: expected error; got nil") - return - } - }() - <-clientDone - <-serverDone -} - -func TestClient_GetTLSConnectionState_noConn(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - _ = c.Close() - _, err = c.GetTLSConnectionState() - if err == nil { - t.Error("GetTLSConnectionState: expected error; got nil") - return - } - }() - <-clientDone - <-serverDone -} - -func TestClient_GetTLSConnectionState_unableErr(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Quit() - }() - c.tls = true - _, err = c.GetTLSConnectionState() - if err == nil { - t.Error("GetTLSConnectionState: expected error; got nil") - return - } - }() - <-clientDone - <-serverDone -} - -func TestClient_HasConnection(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - cfg := &tls.Config{ServerName: "example.com"} - testHookStartTLS(cfg) // set the RootCAs - if err := c.StartTLS(cfg); err != nil { - t.Errorf("StartTLS: %v", err) - return - } - if !c.HasConnection() { - t.Error("HasConnection: expected true; got false") - return - } - if err = c.Quit(); err != nil { - t.Errorf("closing connection failed: %s", err) - return - } - if c.HasConnection() { - t.Error("HasConnection: expected false; got true") - } - }() - <-clientDone - <-serverDone -} - -func TestClient_SetDSNMailReturnOption(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Quit() - }() - c.SetDSNMailReturnOption("foo") - if c.dsnmrtype != "foo" { - t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype) - } - }() - <-clientDone - <-serverDone -} - -func TestClient_SetDSNRcptNotifyOption(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err := serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Quit() - }() - c.SetDSNRcptNotifyOption("foo") - if c.dsnrntype != "foo" { - t.Errorf("SetDSNMailReturnOption: expected %s; got %s", "foo", c.dsnrntype) - } - }() - <-clientDone - <-serverDone -} - -func TestClient_UpdateDeadline(t *testing.T) { - ln := newLocalListener(t) - defer func() { - _ = ln.Close() - }() - clientDone := make(chan bool) - serverDone := make(chan bool) - go func() { - defer close(serverDone) - c, err := ln.Accept() - if err != nil { - t.Errorf("Server accept: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if err = serverHandle(c, t); err != nil { - t.Errorf("server error: %v", err) - } - }() - go func() { - defer close(clientDone) - c, err := Dial(ln.Addr().String()) - if err != nil { - t.Errorf("Client dial: %v", err) - return - } - defer func() { - _ = c.Close() - }() - if !c.HasConnection() { - t.Error("HasConnection: expected true; got false") - return - } - if err = c.UpdateDeadline(time.Millisecond * 20); err != nil { - t.Errorf("failed to update deadline: %s", err) - return - } - time.Sleep(time.Millisecond * 50) - if !c.HasConnection() { - t.Error("HasConnection: expected true; got false") - return - } - }() - <-clientDone - <-serverDone -} - -func newLocalListener(t *testing.T) net.Listener { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - ln, err = net.Listen("tcp6", "[::1]:0") - } - if err != nil { - t.Fatal(err) - } - return ln -} - -type smtpSender struct { - w io.Writer -} - -func (s smtpSender) send(f string) { - _, _ = s.w.Write([]byte(f + "\r\n")) -} - -// smtp server, finely tailored to deal with our own client only! -func serverHandle(c net.Conn, t *testing.T) error { - send := smtpSender{c}.send - send("220 127.0.0.1 ESMTP service ready") - s := bufio.NewScanner(c) - tf := func(config *tls.Config) error { - c = tls.Server(c, config) - defer func() { - _ = c.Close() - }() - return serverHandleTLS(c, t) - } - for s.Scan() { - switch s.Text() { - case "EHLO localhost": - send("250-127.0.0.1 ESMTP offers a warm hug of welcome") - send("250-STARTTLS") - send("250 Ok") - case "STARTTLS": - send("220 Go ahead") - keypair, err := tls.X509KeyPair(localhostCert, localhostKey) - if err != nil { - return err - } - config := &tls.Config{Certificates: []tls.Certificate{keypair}} - return tf(config) - case "QUIT": - return nil - default: - t.Fatalf("unrecognized command: %q", s.Text()) - } - } - return s.Err() -} - -func serverHandleTLS(c net.Conn, t *testing.T) error { - send := smtpSender{c}.send - s := bufio.NewScanner(c) - for s.Scan() { - switch s.Text() { - case "EHLO localhost": - send("250 Ok") - case "MAIL FROM:": - send("250 Ok") - case "RCPT TO:": - send("250 Ok") - case "DATA": - send("354 send the mail data, end with .") - send("250 Ok") - case "Subject: test": - case "": - case "howdy!": - case ".": - case "QUIT": - send("221 127.0.0.1 Service closing transmission channel") - return nil - default: - t.Fatalf("unrecognized command during TLS: %q", s.Text()) - } - } - return s.Err() -} - -func init() { - testRootCAs := x509.NewCertPool() - testRootCAs.AppendCertsFromPEM(localhostCert) - testHookStartTLS = func(config *tls.Config) { - config.RootCAs = testRootCAs - } -} - -func sendMail(hostPort string) error { - from := "joe1@example.com" - to := []string{"joe2@example.com"} - return SendMail(hostPort, nil, from, to, []byte("Subject: test\n\nhowdy!")) -} - -var flaky = flag.Bool("flaky", false, "run known-flaky tests too") - -func SkipFlaky(t testing.TB, issue int) { - t.Helper() - if !*flaky { - t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue) - } -} - - -*/ - // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter From 59f2778a3853e9da3341f5d181beb7c9d98c5d59 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 17:44:31 +0100 Subject: [PATCH 043/125] Add tests for Client's SetDebugLog method These tests verify the behavior of the SetDebugLog method in various scenarios such as enabling and disabling debug logging and ensuring the logger type is as expected. This improves the robustness and reliability of the debug logging functionality in the Client class. --- smtp/smtp_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 11cc2a0..93cbf84 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -29,12 +29,15 @@ import ( "hash" "io" "net" + "os" "strings" "sync/atomic" "testing" "time" "golang.org/x/crypto/pbkdf2" + + "github.com/wneessen/go-mail/log" ) const ( @@ -3117,6 +3120,55 @@ func TestClient_Noop(t *testing.T) { }) } +func TestClient_SetDebugLog(t *testing.T) { + t.Run("set debug loggging to on with no logger defined", func(t *testing.T) { + client := &Client{} + client.SetDebugLog(true) + if !client.debug { + t.Fatalf("expected debug log to be true") + } + if client.logger == nil { + t.Fatalf("expected logger to be defined") + } + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.Stdlog") { + t.Errorf("expected logger to be of type *log.Stdlog, got: %T", client.logger) + } + }) + t.Run("set debug loggging to on should not override logger", func(t *testing.T) { + client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)} + client.SetDebugLog(true) + if !client.debug { + t.Fatalf("expected debug log to be true") + } + if client.logger == nil { + t.Fatalf("expected logger to be defined") + } + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { + t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) + } + }) + t.Run("set debug logggin to off with no logger defined", func(t *testing.T) { + client := &Client{} + client.SetDebugLog(false) + if client.debug { + t.Fatalf("expected debug log to be false") + } + if client.logger != nil { + t.Fatalf("expected logger to be nil") + } + }) + t.Run("set active logging to off should cancel out logger", func(t *testing.T) { + client := &Client{debug: true, logger: log.New(os.Stderr, log.LevelDebug)} + client.SetDebugLog(false) + if client.debug { + t.Fatalf("expected debug log to be false") + } + if client.logger != nil { + t.Fatalf("expected logger to be nil") + } + }) +} + // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter From 9412f318745f79fd4a42e84030f964d4c54f31e0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 17:47:59 +0100 Subject: [PATCH 044/125] Lock mutex when setting the logger Add mutex locking to ensure thread-safety when setting the logger in the `smtp` package. This prevents potential race conditions and ensures that the logger is updated consistently in concurrent operations. --- smtp/smtp.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index 4841ec8..ed86ac9 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -587,7 +587,9 @@ func (c *Client) SetLogger(l log.Logger) { if l == nil { return } + c.mutex.Lock() c.logger = l + c.mutex.Unlock() } // SetLogAuthData enables logging of authentication data in the Client. From 5e3d14f842ab970c58f24971ad58f0881a22dff1 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 17:49:07 +0100 Subject: [PATCH 045/125] Add tests for Client's SetLogger method This commit introduces unit tests for the Client's SetLogger method. It verifies the correct logger type is set and ensures setting a nil logger does not override an existing logger. --- smtp/smtp_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 93cbf84..5b47291 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3169,6 +3169,30 @@ func TestClient_SetDebugLog(t *testing.T) { }) } +func TestClient_SetLogger(t *testing.T) { + t.Run("set logger to Stdlog logger", func(t *testing.T) { + client := &Client{} + client.SetLogger(log.New(os.Stderr, log.LevelDebug)) + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.Stdlog") { + t.Errorf("expected logger to be of type *log.Stdlog, got: %T", client.logger) + } + }) + t.Run("set logger to JSONlog logger", func(t *testing.T) { + client := &Client{} + client.SetLogger(log.NewJSON(os.Stderr, log.LevelDebug)) + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { + t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) + } + }) + t.Run("nil logger should just return and not set/override", func(t *testing.T) { + client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)} + client.SetLogger(nil) + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { + t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) + } + }) +} + // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter From 8fbd94a6755d97feb61a514777b8c51f7dd775b5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:07:31 +0100 Subject: [PATCH 046/125] Add nil check in UpdateDeadline method Ensure that the connection is not nil before setting the deadline in the UpdateDeadline method. This prevents a potential runtime panic when attempting to set a deadline on a nil connection. --- smtp/smtp.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index ed86ac9..8a278ee 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -622,6 +622,9 @@ func (c *Client) HasConnection() bool { func (c *Client) UpdateDeadline(timeout time.Duration) error { c.mutex.Lock() defer c.mutex.Unlock() + if c.conn == nil { + return errors.New("smtp: client has no connection") + } if err := c.conn.SetDeadline(time.Now().Add(timeout)); err != nil { return fmt.Errorf("smtp: failed to update deadline: %w", err) } From 007d214c680d42c2f3111be00fa757b4753ab1e6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:08:56 +0100 Subject: [PATCH 047/125] Add comprehensive tests for SMTP client functionality Introduce new tests to verify behaviors of client methods including SetLogAuthData, SetDSNRcptNotifyOption, SetDSNMailReturnOption, HasConnection, and UpdateDeadline. Ensure these tests cover various scenarios such as successful operations, edge cases, and failure conditions. --- smtp/smtp_test.go | 203 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 5b47291..48a4515 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3193,6 +3193,209 @@ func TestClient_SetLogger(t *testing.T) { }) } +func TestClient_SetLogAuthData(t *testing.T) { + t.Run("set log auth data to true", func(t *testing.T) { + client := &Client{} + client.SetLogAuthData() + if !client.logAuthData { + t.Fatalf("expected log auth data to be true") + } + }) +} + +func TestClient_SetDSNRcptNotifyOption(t *testing.T) { + tests := []string{"NEVER", "SUCCESS", "FAILURE", "DELAY"} + for _, test := range tests { + t.Run("set dsn rcpt notify option to "+test, func(t *testing.T) { + client := &Client{} + client.SetDSNRcptNotifyOption(test) + if !strings.EqualFold(client.dsnrntype, test) { + t.Errorf("expected dsn rcpt notify option to be %s, got %s", test, client.dsnrntype) + } + }) + } +} + +func TestClient_SetDSNMailReturnOption(t *testing.T) { + tests := []string{"HDRS", "FULL"} + for _, test := range tests { + t.Run("set dsn mail return option to "+test, func(t *testing.T) { + client := &Client{} + client.SetDSNMailReturnOption(test) + if !strings.EqualFold(client.dsnmrtype, test) { + t.Errorf("expected dsn mail return option to be %s, got %s", test, client.dsnmrtype) + } + }) + } +} + +func TestClient_HasConnection(t *testing.T) { + t.Run("client has connection", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if !client.HasConnection() { + t.Error("expected client to have a connection") + } + }) + t.Run("client has no connection", func(t *testing.T) { + client := &Client{} + if client.HasConnection() { + t.Error("expected client to have no connection") + } + }) + t.Run("client has no connection after close", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + if client.HasConnection() { + t.Error("expected client to have no connection after close") + } + }) + t.Run("client has no connection after quit", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + if err = client.Quit(); err != nil { + t.Errorf("failed to quit client: %s", err) + } + if client.HasConnection() { + t.Error("expected client to have no connection after quit") + } + }) +} + +func TestClient_UpdateDeadline(t *testing.T) { + t.Run("update deadline on sane client succeeds", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.UpdateDeadline(time.Millisecond * 500); err != nil { + t.Errorf("failed to update connection deadline: %s", err) + } + }) + t.Run("update deadline on no connection should fail", func(t *testing.T) { + client := &Client{} + var err error + if err = client.UpdateDeadline(time.Millisecond * 500); err == nil { + t.Error("expected client deadline update to fail on no connection") + } + expError := "smtp: client has no connection" + if !strings.EqualFold(err.Error(), expError) { + t.Errorf("expected error to be %q, got: %q", expError, err) + } + }) + t.Run("update deadline on closed client should fail", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + if err = client.UpdateDeadline(time.Millisecond * 500); err == nil { + t.Error("expected client deadline update to fail on closed client") + } + }) +} + // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter From 3b9085e19d5742b287d972d56dda1ea6a7fe2bab Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:17:36 +0100 Subject: [PATCH 048/125] Add tests for GetTLSConnectionState method in SMTP client This commit introduces four test cases for the GetTLSConnectionState method. These tests cover scenarios with a valid TLS connection, no connection, a non-TLS connection, and a non-TLS connection with the TLS flag set on the client. This ensures comprehensive validation of the method's behavior across different states. --- smtp/smtp_test.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 48a4515..3fb84de 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3396,6 +3396,122 @@ func TestClient_UpdateDeadline(t *testing.T) { }) } +func TestClient_GetTLSConnectionState(t *testing.T) { + t.Run("get state on sane client connection", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-DSN\r\n250 STARTTLS" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + tlsConfig := getTLSConfig(t) + tlsConfig.MinVersion = tls.VersionTLS12 + tlsConfig.MaxVersion = tls.VersionTLS12 + if err = client.StartTLS(tlsConfig); err != nil { + t.Fatalf("failed to start TLS on client: %s", err) + } + state, err := client.GetTLSConnectionState() + if err != nil { + t.Fatalf("failed to get TLS connection state: %s", err) + } + if state == nil { + t.Error("expected TLS connection state to be non-nil") + } + if state.Version != tls.VersionTLS12 { + t.Errorf("expected TLS connection state version to be %d, got: %d", tls.VersionTLS12, state.Version) + } + }) + t.Run("get state on no connection", func(t *testing.T) { + client := &Client{} + _, err := client.GetTLSConnectionState() + if err == nil { + t.Fatal("expected client to have no tls connection state") + } + }) + t.Run("get state on non-tls client connection", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250 DSN" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + _, err = client.GetTLSConnectionState() + if err == nil { + t.Error("expected client to have no tls connection state") + } + }) + t.Run("fail to get state on non-tls connection with tls flag set", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250 DSN" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }, + ); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) + if err != nil { + t.Fatalf("failed to dial to test server: %s", err) + } + client.tls = true + t.Cleanup(func() { + if err = client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + _, err = client.GetTLSConnectionState() + if err == nil { + t.Error("expected client to have no tls connection state") + } + }) +} + // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter From c58aa354548ced165f2201261b5fe1ceea00e8c2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:25:39 +0100 Subject: [PATCH 049/125] Add tests for Client debug logging behavior Introduce unit tests to verify the behavior of the debug logging in the Client. Confirm that logs are correctly produced when debug mode is enabled and appropriately suppressed when it is disabled. --- smtp/smtp_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 3fb84de..03777d5 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3512,6 +3512,34 @@ func TestClient_GetTLSConnectionState(t *testing.T) { }) } +func TestClient_debugLog(t *testing.T) { + t.Run("debug log is enabled", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + logger := log.New(buffer, log.LevelDebug) + client := &Client{logger: logger, debug: true} + client.debugLog(log.DirClientToServer, "%s", "simple string") + client.debugLog(log.DirServerToClient, "%d", 1234) + want := "DEBUG: C --> S: simple string" + if !strings.Contains(buffer.String(), want) { + t.Errorf("expected debug log to contain %q, got: %q", want, buffer.String()) + } + want = "DEBUG: C <-- S: 1234" + if !strings.Contains(buffer.String(), want) { + t.Errorf("expected debug log to contain %q, got: %q", want, buffer.String()) + } + }) + t.Run("debug log is disable", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + logger := log.New(buffer, log.LevelDebug) + client := &Client{logger: logger, debug: false} + client.debugLog(log.DirClientToServer, "%s", "simple string") + client.debugLog(log.DirServerToClient, "%d", 1234) + if buffer.Len() > 0 { + t.Errorf("expected debug log to be empty, got: %q", buffer.String()) + } + }) +} + // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter From 7da30e09e1c46cf3519905f75a22f1f2ac7f2a0d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:35:10 +0100 Subject: [PATCH 050/125] Refactor smtp tests to improve clarity and error handling Removed unused variables and improved error handling in smtp_test.go. Adjusted to capture only error in auth.Next() calls, ensuring accurate validation. Added necessary error checks after creating new client connections to prevent test failures. --- smtp/smtp_test.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 03777d5..9011363 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -670,7 +670,7 @@ func TestLoginAuth(t *testing.T) { if !bytes.Equal([]byte(pass), resp) { t.Errorf("expected response to second challange to be: %q, got: %q", pass, resp) } - resp, err = auth.Next([]byte("nonsense"), true) + _, err = auth.Next([]byte("nonsense"), true) if err == nil { t.Error("expected third server challange to fail, but didn't") } @@ -818,7 +818,7 @@ func TestLoginAuth_noEnc(t *testing.T) { if !bytes.Equal([]byte(pass), resp) { t.Errorf("expected response to second challange to be: %q, got: %q", pass, resp) } - resp, err = auth.Next([]byte("nonsense"), true) + _, err = auth.Next([]byte("nonsense"), true) if err == nil { t.Error("expected third server challange to fail, but didn't") } @@ -910,7 +910,7 @@ func TestXOAuth2Auth(t *testing.T) { if !bytes.Equal([]byte(""), resp) { t.Errorf("expected server response to be empty, got: %q", resp) } - resp, err = auth.Next([]byte("nonsense"), false) + _, err = auth.Next([]byte("nonsense"), false) if err != nil { t.Errorf("failed on first server challange: %s", err) } @@ -1107,6 +1107,9 @@ func TestScramAuth(t *testing.T) { t.Fatalf("failed to dial TLS server: %v", err) } client, err = NewClient(conn, TestServerAddr) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } case false: var err error client, err = Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -1176,6 +1179,9 @@ func TestScramAuth(t *testing.T) { t.Fatalf("failed to dial TLS server: %v", err) } client, err = NewClient(conn, TestServerAddr) + if err != nil { + t.Fatalf("failed to connect to test server: %s", err) + } case false: var err error client, err = Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -3434,7 +3440,7 @@ func TestClient_GetTLSConnectionState(t *testing.T) { t.Fatalf("failed to get TLS connection state: %s", err) } if state == nil { - t.Error("expected TLS connection state to be non-nil") + t.Fatal("expected TLS connection state to be non-nil") } if state.Version != tls.VersionTLS12 { t.Errorf("expected TLS connection state version to be %d, got: %d", tls.VersionTLS12, state.Version) @@ -3543,7 +3549,6 @@ func TestClient_debugLog(t *testing.T) { // faker is a struct embedding io.ReadWriter to simulate network connections for testing purposes. type faker struct { io.ReadWriter - failOnRead bool failOnClose bool } @@ -3865,10 +3870,9 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server // fields are present. We have actual real authentication tests for all SCRAM modes in the // go-mail client_test.go type testSCRAMSMTP struct { - authMechanism string - nonce string - h func() hash.Hash - tlsServer bool + nonce string + h func() hash.Hash + tlsServer bool } func (s *testSCRAMSMTP) handleSCRAMAuth(conn net.Conn) { From 61353d51e56656d659762285e6d694cd48a64397 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:43:48 +0100 Subject: [PATCH 051/125] Fix variable declarations in test cases Changed variable declarations from '=' to ':=' to properly handle errors within the SMTP test cases. This ensures that errors are correctly captured and reported when writing to the EchoBuffer. --- smtp/smtp_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 9011363..9ac6118 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3692,7 +3692,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } time.Sleep(time.Millisecond) if props.EchoBuffer != nil { - if _, err = props.EchoBuffer.Write([]byte(data)); err != nil { + if _, err := props.EchoBuffer.Write([]byte(data)); err != nil { t.Errorf("failed write to echo buffer: %s", err) } } @@ -3795,7 +3795,7 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } if props.EchoBuffer != nil { - if _, err = props.EchoBuffer.Write([]byte(ddata)); err != nil { + if _, err := props.EchoBuffer.Write([]byte(ddata)); err != nil { t.Errorf("failed write to echo buffer: %s", err) } } From 2156fbc01ee1c6a90d026ade835e27390c70c8b3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:51:14 +0100 Subject: [PATCH 052/125] Add mutex lock to handle concurrent SMTP test server connections Introduced a mutex to the SMTP test server properties to ensure thread-safe access when handling connections. This prevents race conditions and improves the reliability of the test server under concurrent load. --- smtp/smtp_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 9ac6118..ee1ae5a 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -31,6 +31,7 @@ import ( "net" "os" "strings" + "sync" "sync/atomic" "testing" "time" @@ -3592,6 +3593,7 @@ type serverProps struct { SSLListener bool TestSCRAM bool VRFYUserUnknown bool + mutex sync.Mutex } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -3643,7 +3645,9 @@ func simpleSMTPServer(ctx context.Context, t *testing.T, props *serverProps) err } return fmt.Errorf("unable to accept connection: %w", err) } + props.mutex.Lock() handleTestServerConnection(connection, t, props) + props.mutex.Unlock() } } } From 08034e6ff894b5cf9a36e3c5325ece0dc6604b0a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 18:58:31 +0100 Subject: [PATCH 053/125] Refactor echoBuffer parameter handling in tests Removed redundant mutex and streamlined anonymous goroutine syntax for test server setup by passing echoBuffer directly as a parameter. This change reduces unnecessary use of shared resources and simplifies the test code structure and fixes potential race conditions --- smtp/smtp_test.go | 52 ++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index ee1ae5a..e10748b 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -31,7 +31,6 @@ import ( "net" "os" "strings" - "sync" "sync/atomic" "testing" "time" @@ -2236,9 +2235,9 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-8BITMIME\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2246,7 +2245,7 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2274,9 +2273,9 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-SMTPUTF8\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2284,7 +2283,7 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2312,9 +2311,9 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-SMTPUTF8\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2322,7 +2321,7 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2350,9 +2349,9 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2360,7 +2359,7 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2389,9 +2388,9 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250-8BITMIME\r\n250-SMTPUTF8\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2399,7 +2398,7 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2491,9 +2490,9 @@ func TestClient_Rcpt(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2501,7 +2500,7 @@ func TestClient_Rcpt(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { @@ -2783,9 +2782,9 @@ func TestSendMail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2793,7 +2792,7 @@ func TestSendMail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) testHookStartTLS = func(config *tls.Config) { @@ -2858,9 +2857,9 @@ func TestSendMail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func() { + go func(buf *bytes.Buffer) { if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: echoBuffer, + EchoBuffer: buf, FeatureSet: featureSet, ListenPort: serverPort, }, @@ -2868,7 +2867,7 @@ func TestSendMail(t *testing.T) { t.Errorf("failed to start test server: %s", err) return } - }() + }(echoBuffer) time.Sleep(time.Millisecond * 30) addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) testHookStartTLS = func(config *tls.Config) { @@ -3593,7 +3592,6 @@ type serverProps struct { SSLListener bool TestSCRAM bool VRFYUserUnknown bool - mutex sync.Mutex } // simpleSMTPServer starts a simple TCP server that resonds to SMTP commands. @@ -3645,9 +3643,7 @@ func simpleSMTPServer(ctx context.Context, t *testing.T, props *serverProps) err } return fmt.Errorf("unable to accept connection: %w", err) } - props.mutex.Lock() handleTestServerConnection(connection, t, props) - props.mutex.Unlock() } } } From 77175a2952c64f6eea5c035eba1cea74de463225 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 19:15:13 +0100 Subject: [PATCH 054/125] Refactor and relocate JSON logger tests for Go 1.21 compliance Removed JSON logger tests from smtp_test.go and relocated them to a new file smtp_121_test.go, ensuring compliance with Go 1.21. This change maintains test integrity while organizing tests by Go version compatibility. --- smtp/smtp_121_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++ smtp/smtp_test.go | 27 -------------------- 2 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 smtp/smtp_121_test.go diff --git a/smtp/smtp_121_test.go b/smtp/smtp_121_test.go new file mode 100644 index 0000000..ed722e0 --- /dev/null +++ b/smtp/smtp_121_test.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2010 The Go Authors. All rights reserved. +// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors +// +// Original net/smtp code from the Go stdlib by the Go Authors. +// Use of this source code is governed by a BSD-style +// LICENSE file that can be found in this directory. +// +// go-mail specific modifications by the go-mail Authors. +// Licensed under the MIT License. +// See [PROJECT ROOT]/LICENSES directory for more information. +// +// SPDX-License-Identifier: BSD-3-Clause AND MIT + +//go:build go1.21 +// +build go1.21 + +package smtp + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/wneessen/go-mail/log" +) + +func TestClient_SetDebugLog_JSON(t *testing.T) { + t.Run("set debug loggging to on should not override logger", func(t *testing.T) { + client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)} + client.SetDebugLog(true) + if !client.debug { + t.Fatalf("expected debug log to be true") + } + if client.logger == nil { + t.Fatalf("expected logger to be defined") + } + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { + t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) + } + }) +} + +func TestClient_SetLogger_JSON(t *testing.T) { + t.Run("set logger to JSONlog logger", func(t *testing.T) { + client := &Client{} + client.SetLogger(log.NewJSON(os.Stderr, log.LevelDebug)) + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { + t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) + } + }) + t.Run("nil logger should just return and not set/override", func(t *testing.T) { + client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)} + client.SetLogger(nil) + if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { + t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) + } + }) +} diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index e10748b..4d6316a 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3140,19 +3140,6 @@ func TestClient_SetDebugLog(t *testing.T) { t.Errorf("expected logger to be of type *log.Stdlog, got: %T", client.logger) } }) - t.Run("set debug loggging to on should not override logger", func(t *testing.T) { - client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)} - client.SetDebugLog(true) - if !client.debug { - t.Fatalf("expected debug log to be true") - } - if client.logger == nil { - t.Fatalf("expected logger to be defined") - } - if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { - t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) - } - }) t.Run("set debug logggin to off with no logger defined", func(t *testing.T) { client := &Client{} client.SetDebugLog(false) @@ -3183,20 +3170,6 @@ func TestClient_SetLogger(t *testing.T) { t.Errorf("expected logger to be of type *log.Stdlog, got: %T", client.logger) } }) - t.Run("set logger to JSONlog logger", func(t *testing.T) { - client := &Client{} - client.SetLogger(log.NewJSON(os.Stderr, log.LevelDebug)) - if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { - t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) - } - }) - t.Run("nil logger should just return and not set/override", func(t *testing.T) { - client := &Client{logger: log.NewJSON(os.Stderr, log.LevelDebug)} - client.SetLogger(nil) - if !strings.EqualFold(fmt.Sprintf("%T", client.logger), "*log.JSONlog") { - t.Errorf("expected logger to be of type *log.JSONlog, got: %T", client.logger) - } - }) } func TestClient_SetLogAuthData(t *testing.T) { From f7bdd8fffc66a2fd380d8a7035dba0ba709360dd Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 19:21:50 +0100 Subject: [PATCH 055/125] Refactor error variable in smtp test Renamed 'err' to 'berr' to avoid shadowing outer variable. This change ensures clearer error handling and avoids potential issues with variable scope and readability in the smtp_test.go file. --- smtp/smtp_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 4d6316a..15ea7c5 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -3665,8 +3665,8 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } time.Sleep(time.Millisecond) if props.EchoBuffer != nil { - if _, err := props.EchoBuffer.Write([]byte(data)); err != nil { - t.Errorf("failed write to echo buffer: %s", err) + if _, berr := props.EchoBuffer.Write([]byte(data)); berr != nil { + t.Errorf("failed write to echo buffer: %s", berr) } } From 800c266ccba97415266df0ccc0cbdf0e71cabcc3 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 19:53:14 +0100 Subject: [PATCH 056/125] Refactor test server code for thread safety Moved serverProps outside goroutines to improve code readability and maintainability. Added a RWMutex to serverProps to ensure thread-safe access to EchoBuffer, preventing race conditions during concurrent writes. --- smtp/smtp_test.go | 160 ++++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 68 deletions(-) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 15ea7c5..931b74e 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -31,6 +31,7 @@ import ( "net" "os" "strings" + "sync" "sync/atomic" "testing" "time" @@ -2235,17 +2236,17 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-8BITMIME\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2261,7 +2262,9 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: BODY=8BITMIME" + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if !strings.EqualFold(resp[5], expected) { t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } @@ -2273,17 +2276,17 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-SMTPUTF8\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2299,7 +2302,9 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: SMTPUTF8" + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if !strings.EqualFold(resp[5], expected) { t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } @@ -2311,17 +2316,17 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-SMTPUTF8\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2337,7 +2342,9 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: SMTPUTF8" + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if !strings.EqualFold(resp[5], expected) { t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } @@ -2349,17 +2356,17 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2376,7 +2383,9 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: RET=FULL" + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if !strings.EqualFold(resp[5], expected) { t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5]) } @@ -2388,17 +2397,17 @@ func TestClient_Mail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250-8BITMIME\r\n250-SMTPUTF8\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) @@ -2415,7 +2424,9 @@ func TestClient_Mail(t *testing.T) { t.Errorf("failed to set mail from address: %s", err) } expected := "MAIL FROM: BODY=8BITMIME SMTPUTF8 RET=FULL" + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if !strings.EqualFold(resp[7], expected) { t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[7]) } @@ -2490,17 +2501,17 @@ func TestClient_Rcpt(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) client, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, serverPort)) if err != nil { @@ -2519,7 +2530,9 @@ func TestClient_Rcpt(t *testing.T) { t.Error("recpient address with newlines should fail") } expected := "RCPT TO: NOTIFY=SUCCESS" + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if !strings.EqualFold(resp[5], expected) { t.Errorf("expected rcpt to command to be %q, but sent %q", expected, resp[5]) } @@ -2782,17 +2795,17 @@ func TestSendMail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) testHookStartTLS = func(config *tls.Config) { @@ -2806,7 +2819,9 @@ func TestSendMail(t *testing.T) { []byte("test message")); err != nil { t.Fatalf("failed to send mail: %s", err) } + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if len(resp)-1 != len(want) { t.Fatalf("expected %d lines, but got %d", len(want), len(resp)) } @@ -2857,17 +2872,17 @@ func TestSendMail(t *testing.T) { serverPort := int(TestServerPortBase + PortAdder.Load()) featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS" echoBuffer := bytes.NewBuffer(nil) - go func(buf *bytes.Buffer) { - if err := simpleSMTPServer(ctx, t, &serverProps{ - EchoBuffer: buf, - FeatureSet: featureSet, - ListenPort: serverPort, - }, - ); err != nil { + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctx, t, props); err != nil { t.Errorf("failed to start test server: %s", err) return } - }(echoBuffer) + }() time.Sleep(time.Millisecond * 30) addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort) testHookStartTLS = func(config *tls.Config) { @@ -2887,7 +2902,9 @@ Goodbye.`) if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"}, message); err != nil { t.Fatalf("failed to send mail: %s", err) } + props.BufferMutex.RLock() resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() if len(resp)-1 != len(want) { t.Errorf("expected %d lines, but got %d", len(want), len(resp)) } @@ -3542,6 +3559,7 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { + BufferMutex sync.RWMutex EchoBuffer io.Writer FailOnAuth bool FailOnDataInit bool @@ -3640,9 +3658,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server t.Logf("failed to write line: %s", err) } if props.EchoBuffer != nil { - if _, err := props.EchoBuffer.Write([]byte(data + "\r\n")); err != nil { - t.Errorf("failed write to echo buffer: %s", err) + props.BufferMutex.Lock() + if _, berr := props.EchoBuffer.Write([]byte(data + "\r\n")); berr != nil { + t.Errorf("failed write to echo buffer: %s", berr) } + props.BufferMutex.Unlock() } _ = writer.Flush() } @@ -3665,9 +3685,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server } time.Sleep(time.Millisecond) if props.EchoBuffer != nil { + props.BufferMutex.Lock() if _, berr := props.EchoBuffer.Write([]byte(data)); berr != nil { t.Errorf("failed write to echo buffer: %s", berr) } + props.BufferMutex.Unlock() } var datastring string @@ -3768,9 +3790,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } if props.EchoBuffer != nil { - if _, err := props.EchoBuffer.Write([]byte(ddata)); err != nil { - t.Errorf("failed write to echo buffer: %s", err) + props.BufferMutex.Lock() + if _, berr := props.EchoBuffer.Write([]byte(ddata)); berr != nil { + t.Errorf("failed write to echo buffer: %s", berr) } + props.BufferMutex.Unlock() } ddata = strings.TrimSpace(ddata) if ddata == "." { From ad86c7ac4fb00cb3cd98ba50f71f9211e4a6e231 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 20:06:07 +0100 Subject: [PATCH 057/125] Correct typo in test name The test name had a typo in the word "recipient." This commit corrects "recepient" to "recipient" to improve code readability and maintain consistency in naming conventions. This change does not modify any functional behavior of the tests. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 96ebf1f..39bd7e0 100644 --- a/client_test.go +++ b/client_test.go @@ -2803,7 +2803,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("expected ErrGetSender, got %s", sendErr.Reason) } }) - t.Run("fail with no recepient addresses", func(t *testing.T) { + t.Run("fail with no recipient addresses", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() PortAdder.Add(1) From 79d4c6fd0708d70861e1083c1a2cc41ba3a1a23b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 20:06:47 +0100 Subject: [PATCH 058/125] Correct spelling errors in email validation comments Fixed the spelling of "dot-separated" in comment explanations to ensure clarity. This makes the comments more accurate and easier to understand. --- msg_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msg_test.go b/msg_test.go index 117e8e7..ce98447 100644 --- a/msg_test.go +++ b/msg_test.go @@ -126,8 +126,8 @@ var ( {`" "@domain.tld`, true}, // Still valid, since quoted {`"<\"@\".!#%$@domain.tld"`, false}, // Quoting with illegal characters is not allowed {`<\"@\\".!#%$@domain.tld`, false}, // Still a bunch of random illegal characters - {`hi"@"there@domain.tld`, false}, // Quotes must be dot-seperated - {`"<\"@\\".!.#%$@domain.tld`, false}, // Quote is escaped and dot-seperated which would be RFC822 compliant, but not RFC5322 compliant + {`hi"@"there@domain.tld`, false}, // Quotes must be dot-separated + {`"<\"@\\".!.#%$@domain.tld`, false}, // Quote is escaped and dot-separated which would be RFC822 compliant, but not RFC5322 compliant {`hi\ there@domain.tld`, false}, // Spaces must be quoted {"hello@tld", true}, // TLD is enough {`你好@域名.顶级域名`, true}, // We speak RFC6532 From c7438a974cf8e3fac2119bec19da28c899907990 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 20:07:55 +0100 Subject: [PATCH 059/125] Fix typos: change "non-existant" to "non-existent" Corrected multiple instances of the word "non-existant" to "non-existent" in test descriptions to improve code clarity and accuracy. This change affects both smtp_test.go and msg_test.go files. --- msg_test.go | 8 ++++---- smtp/smtp_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/msg_test.go b/msg_test.go index ce98447..3f2cf94 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4527,12 +4527,12 @@ func TestMsg_AttachFile(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) } }) - t.Run("AttachFile with non-existant file", func(t *testing.T) { + t.Run("AttachFile with non-existent file", func(t *testing.T) { message := NewMsg() if message == nil { t.Fatal("message is nil") } - message.AttachFile("testdata/non-existant-file.txt") + message.AttachFile("testdata/non-existent-file.txt") attachments := message.GetAttachments() if len(attachments) != 0 { t.Fatalf("failed to retrieve attachments list") @@ -4997,12 +4997,12 @@ func TestMsg_EmbedFile(t *testing.T) { t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) } }) - t.Run("EmbedFile with non-existant file", func(t *testing.T) { + t.Run("EmbedFile with non-existent file", func(t *testing.T) { message := NewMsg() if message == nil { t.Fatal("message is nil") } - message.EmbedFile("testdata/non-existant-file.txt") + message.EmbedFile("testdata/non-existent-file.txt") embeds := message.GetEmbeds() if len(embeds) != 0 { t.Fatalf("failed to retrieve attachments list") diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 931b74e..1b616fc 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -1492,7 +1492,7 @@ func TestNewClient(t *testing.T) { t.Run("new client via Dial fails on server not started", func(t *testing.T) { _, err := Dial(fmt.Sprintf("%s:%d", TestServerAddr, 64000)) if err == nil { - t.Error("dial on non-existant server should fail") + t.Error("dial on non-existent server should fail") } }) t.Run("new client fails on server not available", func(t *testing.T) { From 29794fd6ad5167bdd7d334bec8960dabe77577df Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 20:08:58 +0100 Subject: [PATCH 060/125] Fix typo in Content-Disposition header in tests Corrected multiple instances of the misspelled "Content-Dispositon" to "Content-Disposition" in the msgwriter_test.go file. This ensures that the error messages in the tests are accurate and improve code readability. --- msgwriter_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index 330507b..1b2a254 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -324,7 +324,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } } if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) { - t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String()) + t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String()) } switch runtime.GOOS { case "freebsd": @@ -357,7 +357,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } } if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment"`) { - t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String()) + t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String()) } if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment"`) { t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String()) @@ -383,7 +383,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } } if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) { - t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String()) + t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String()) } if !strings.Contains(buffer.String(), `Content-Type: application/octet-stream; name="attachment.txt"`) { t.Errorf("Content-Type header not found for attachment. Mail: %s", buffer.String()) @@ -402,7 +402,7 @@ func TestMsgWriter_addFiles(t *testing.T) { t.Errorf("attachment not found in mail message. Mail: %s", buffer.String()) } if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) { - t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String()) + t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String()) } switch runtime.GOOS { case "freebsd": @@ -438,7 +438,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } } if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) { - t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String()) + t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String()) } switch runtime.GOOS { case "freebsd": @@ -478,7 +478,7 @@ func TestMsgWriter_addFiles(t *testing.T) { } } if !strings.Contains(buffer.String(), `Content-Disposition: attachment; filename="attachment.txt"`) { - t.Errorf("Content-Dispositon header not found for attachment. Mail: %s", buffer.String()) + t.Errorf("Content-Disposition header not found for attachment. Mail: %s", buffer.String()) } switch runtime.GOOS { case "freebsd": From 935a523fa77bda465ac0fc21d8dbde579fc5338d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 20:16:09 +0100 Subject: [PATCH 061/125] Change license to MIT Updated SPDX identifiers from CC0-1.0 to MIT across multiple files, including `.github`, `CONTRIBUTING.md`, `README.md`, and more. Deleted the `LICENSES/CC0-1.0.txt` file as it is no longer relevant. --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/dependabot.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .gitignore | 2 +- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 2 +- LICENSES/CC0-1.0.txt | 121 --------------------- README.md | 2 +- SECURITY.md | 2 +- codecov.yml | 6 +- sonar-project.properties | 2 +- 15 files changed, 16 insertions(+), 137 deletions(-) delete mode 100644 LICENSES/CC0-1.0.txt diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e1400c1..8212de2 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Winni Neessen # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT github: wneessen ko_fi: winni diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa1cd05..2117d21 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Winni Neessen # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT name: Bug Report description: Create a report to help us improve diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f6ea6d2..f086a05 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Winni Neessen # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT blank_issues_enabled: false contact_links: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d2dae5e..e44cbd5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Winni Neessen # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT name: Feature request description: Suggest an idea for this project diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c29d473..5c38fa1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022-2023 The go-mail Authors # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT version: 2 updates: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 195e5e8..9c8a635 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Winni Neessen # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 61f7553..7a500c1 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022-2023 The go-mail Authors # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT # This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy diff --git a/.gitignore b/.gitignore index 5ce8347..ad05dc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022 Winni Neessen # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT # Binaries for programs and plugins *.exe diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index be485b6..a188a88 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,7 +1,7 @@ # Contributor Covenant Code of Conduct diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 959134a..c8f7c00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # How to contribute diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt deleted file mode 100644 index 0e259d4..0000000 --- a/LICENSES/CC0-1.0.txt +++ /dev/null @@ -1,121 +0,0 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. diff --git a/README.md b/README.md index 3a67d0d..aae49ee 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # go-mail - Easy to use, yet comprehensive library for sending mails with Go diff --git a/SECURITY.md b/SECURITY.md index e2059c2..b0ed1ca 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,7 +1,7 @@ # Security Policy diff --git a/codecov.yml b/codecov.yml index a9f998e..7c99de7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,19 +1,19 @@ # SPDX-FileCopyrightText: 2022-2023 The go-mail Authors # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT coverage: status: project: default: - target: 90% + target: 95% threshold: 2% base: auto if_ci_failed: error only_pulls: false patch: default: - target: 90% + target: 95% base: auto if_ci_failed: error threshold: 2% diff --git a/sonar-project.properties b/sonar-project.properties index e228a11..b800d26 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2022-2023 The go-mail Authors # -# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT sonar.projectKey=go-mail sonar.go.coverage.reportPaths=cov.out From 6c06c459dae511ced695ee1c7770b09fd2b2623c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 20:23:42 +0100 Subject: [PATCH 062/125] Update README.md SMTP features and TLS support details Revised the documentation for greater clarity: swapped the listed items for explicit and implicit SSL/TLS support, and detailed the supported SMTP authentication mechanisms in an itemized format. This ensures users understand all available options and configurations more clearly. --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aae49ee..947b6f2 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,16 @@ Here are some highlights of go-mail's featureset: * [X] Very small dependency footprint (mainly Go Stdlib and Go extended packages) * [X] Modern, idiomatic Go * [X] Sane and secure defaults -* [X] Explicit SSL/TLS support -* [X] Implicit StartTLS support with different policies +* [X] Implicit SSL/TLS support +* [X] Explicit STARTTLS support with different policies * [X] Makes use of contexts for a better control flow and timeout/cancelation handling -* [X] SMTP Auth support (LOGIN, PLAIN, CRAM-MD, XOAUTH2, SCRAM-SHA-1(-PLUS), SCRAM-SHA-256(-PLUS)) +* [X] SMTP Auth support + * [X] CRAM-MD5 + * [X] LOGIN + * [X] PLAIN + * [X] SCRAM-SHA-1/SCRAM-SHA-1-PLUS + * [X] SCRAM-SHA-256/SCRAM-SHA-256-PLUS + * [X] XOAUTH2 * [X] RFC5322 compliant mail address validation * [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.) * [X] Concurrency-safe reusing the same SMTP connection to send multiple mails From cff789883f414ec09853b7a1bc472792274be9c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:13:53 +0000 Subject: [PATCH 063/125] Bump github/codeql-action from 3.27.1 to 3.27.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.1 to 3.27.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4f3212b61783c3c68e8309a0f18a699764811cda...9278e421667d5d90a2839487a482448c4ec7df4d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9c8a635..e41bf58 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/init@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/autobuild@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -79,4 +79,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/analyze@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 7a500c1..2d14abe 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 with: sarif_file: results.sarif From ea70b21c904d0817ca990e9fc94a6058ad8d19fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:31:45 +0000 Subject: [PATCH 064/125] Bump sonarsource/sonarqube-scan-action from 3.1.0 to 4.0.0 Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 3.1.0 to 4.0.0. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/13990a695682794b53148ff9f6a8b6e22e43955e...94d4f8ac4aaefccd7fb84bff00b0aeb2d65fcd49) --- updated-dependencies: - dependency-name: sonarsource/sonarqube-scan-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a842e9..8a43e96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -208,7 +208,7 @@ jobs: run: | go test -shuffle=on -race --coverprofile=./cov.out ./... - name: SonarQube scan - uses: sonarsource/sonarqube-scan-action@13990a695682794b53148ff9f6a8b6e22e43955e # master + uses: sonarsource/sonarqube-scan-action@94d4f8ac4aaefccd7fb84bff00b0aeb2d65fcd49 # master if: success() env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 2f97ebabd3bc5c0ac284074c6df414be003c4e89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:31:52 +0000 Subject: [PATCH 065/125] Bump github/codeql-action from 3.27.2 to 3.27.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.2 to 3.27.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/9278e421667d5d90a2839487a482448c4ec7df4d...396bb3e45325a47dd9ef434068033c6d5bb0d11a) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e41bf58..9a4b504 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 + uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 + uses: github/codeql-action/autobuild@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -79,4 +79,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 + uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 2d14abe..fe9c6b2 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 + uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 with: sarif_file: results.sarif From 6809084e80e59735a5c6d9787cc755a721695f8c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 16:11:28 +0100 Subject: [PATCH 066/125] Add enhanced status code and error code to SendError Enhance error handling by adding error code and enhanced status code to the SendError struct. This allows for better troubleshooting and debugging by providing more detailed SMTP server responses. --- client.go | 24 ++++++++++++----- client_test.go | 53 +++++++++++++++++++++++++++++++++++++ senderror.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index fbb28f5..d64c172 100644 --- a/client.go +++ b/client.go @@ -1190,6 +1190,7 @@ func (c *Client) auth() error { func (c *Client) sendSingleMsg(message *Msg) error { c.mutex.Lock() defer c.mutex.Unlock() + escSupport, _ := c.smtpClient.Extension("ENHANCEDSTATUSCODES") if message.encoding == NoEncoding { if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { @@ -1200,14 +1201,16 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err != nil { return &SendError{ Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } } rcpts, err := message.GetRecipients() if err != nil { return &SendError{ Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } } @@ -1219,7 +1222,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err = c.smtpClient.Mail(from); err != nil { retError := &SendError{ Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { retError.errlist = append(retError.errlist, resetSendErr) @@ -1238,6 +1242,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { rcptSendErr.errlist = append(rcptSendErr.errlist, err) rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) rcptSendErr.isTemp = isTempError(err) + rcptSendErr.errcode = getErrorCode(err) + rcptSendErr.enhancedStatusCode = getEnhancedStatusCode(err, escSupport) hasError = true } } @@ -1251,20 +1257,23 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err != nil { return &SendError{ Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } } _, err = message.WriteTo(writer) if err != nil { return &SendError{ Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } } if err = writer.Close(); err != nil { return &SendError{ Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } } message.isDelivered = true @@ -1272,7 +1281,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err = c.Reset(); err != nil { return &SendError{ Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, + affectedMsg: message, errcode: getErrorCode(err), + enhancedStatusCode: getEnhancedStatusCode(err, escSupport), } } return nil diff --git a/client_test.go b/client_test.go index 39bd7e0..2687611 100644 --- a/client_test.go +++ b/client_test.go @@ -3148,6 +3148,59 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("expected ErrSMTPDataClose, got %s", sendErr.Reason) } }) + t.Run("error code and enhanced status code support", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FailOnMailFrom: true, + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + message := testMessage(t) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + if err = client.DialWithContext(ctxDial); err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + t.Skip("failed to connect to the test server due to timeout") + } + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("failed to close client: %s", err) + } + }) + if err = client.sendSingleMsg(message); err == nil { + t.Error("expected mail delivery to fail") + } + var sendErr *SendError + if !errors.As(err, &sendErr) { + t.Fatalf("expected SendError, got %s", err) + } + if sendErr.errcode != 500 { + t.Errorf("expected error code 500, got %d", sendErr.errcode) + } + if !strings.EqualFold(sendErr.enhancedStatusCode, "5.5.2") { + t.Errorf("expected enhanced status code 5.5.2, got %s", sendErr.enhancedStatusCode) + } + }) } func TestClient_checkConn(t *testing.T) { diff --git a/senderror.go b/senderror.go index 4471208..a371c6e 100644 --- a/senderror.go +++ b/senderror.go @@ -6,6 +6,8 @@ package mail import ( "errors" + "regexp" + "strconv" "strings" ) @@ -60,11 +62,13 @@ const ( // details about the affected message, a list of errors, the recipient list, and whether // the error is temporary or permanent. It also includes a reason code for the error. type SendError struct { - affectedMsg *Msg - errlist []error - isTemp bool - rcpt []string - Reason SendErrReason + affectedMsg *Msg + errcode int + enhancedStatusCode string + errlist []error + isTemp bool + rcpt []string + Reason SendErrReason } // SendErrReason represents a comparable reason on why the delivery failed @@ -175,6 +179,27 @@ func (e *SendError) Msg() *Msg { return e.affectedMsg } +// EnhancedStatusCode returns the enhanced status code of the server response if the +// server supports it, as described in RFC 2034. +// +// This function retrieves the enhanced status code of an error returned by the server. This +// requires that the receiving server supports this SMTP extension as described in RFC 2034. +// Since this is the SendError interface, we only collect status codes for error responses, +// meaning 4xx or 5xx. If the server does not support the ENHANCEDSTATUSCODES extension or +// the error did not include an enhanced status code, it will return an empty string. +// +// Returns: +// - The enhanced status code as returned by the server, or an empty string is not supported. +// +// References: +// - https://datatracker.ietf.org/doc/html/rfc2034 +func (e *SendError) EnhancedStatusCode() string { + if e == nil { + return "" + } + return e.enhancedStatusCode +} + // String satisfies the fmt.Stringer interface for the SendErrReason type. // // This function converts the SendErrReason into a human-readable string representation based @@ -224,3 +249,39 @@ func (r SendErrReason) String() string { func isTempError(err error) bool { return err.Error()[0] == '4' } + +func getErrorCode(err error) int { + rootErr := errors.Unwrap(err) + if rootErr != nil { + err = rootErr + } + firstrune := err.Error()[0] + if firstrune < 52 || firstrune > 53 { + return 0 + } + code := err.Error()[0:3] + errcode, cerr := strconv.Atoi(code) + if cerr != nil { + return 0 + } + return errcode +} + +func getEnhancedStatusCode(err error, supported bool) string { + if err == nil || !supported { + return "" + } + rootErr := errors.Unwrap(err) + if rootErr != nil { + err = rootErr + } + firstrune := err.Error()[0] + if firstrune != 50 && firstrune != 52 && firstrune != 53 { + return "" + } + re, rerr := regexp.Compile(`\b([245])\.\d{1,3}\.\d{1,3}\b`) + if rerr != nil { + return "" + } + return re.FindString(err.Error()) +} From b9d94492521beb8f740542e723f19b80e71cf30c Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 21:25:39 +0100 Subject: [PATCH 067/125] Change test server port base for SMTP client tests Updated the TestServerPortBase from 12025 to 30025 to avoid port conflicts with other services running on the common 12025 port. This adjustment aims to ensure that the tests run reliably in diverse environments. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index 2687611..d1afc86 100644 --- a/client_test.go +++ b/client_test.go @@ -35,7 +35,7 @@ const ( // 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 = 12025 + TestServerPortBase = 30025 // TestSenderValid is a test sender email address considered valid for sending test emails. TestSenderValid = "valid-from@domain.tld" // TestRcptValid is a test recipient email address considered valid for sending test emails. From ad265cac57670289aedacd5acc27331d7a98d1df Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 21:26:11 +0100 Subject: [PATCH 068/125] Add ErrorCode method to SendError Implemented ErrorCode method to retrieve the error code from the server response in SendError. This method distinguishes between server-generated errors and client-generated errors, returning 0 for errors generated by the client. --- senderror.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/senderror.go b/senderror.go index a371c6e..93ab6f2 100644 --- a/senderror.go +++ b/senderror.go @@ -200,6 +200,21 @@ func (e *SendError) EnhancedStatusCode() string { return e.enhancedStatusCode } +// ErrorCode returns the error code of the server response. +// +// This function retrieves the error code the error returned by the server. The error code will +// start with 5 on permanent errors and with 4 on a temporary error. If the error is not returned +// by the server, but is generated by go-mail, the code will be 0. +// +// Returns: +// - The error code as returned by the server, or 0 if not a server error. +func (e *SendError) ErrorCode() int { + if e == nil { + return 0 + } + return e.errcode +} + // String satisfies the fmt.Stringer interface for the SendErrReason type. // // This function converts the SendErrReason into a human-readable string representation based From 615155bfc2f34c1d085bc7fe2fbae5bd6bccf36d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 21:27:27 +0100 Subject: [PATCH 069/125] Add tests for SendError's enhanced status and error codes Implemented new unit tests for SendError to validate the enhanced status code and error codes in various scenarios, including nil SendError cases, errors with no enhanced status code, and errors with both permanent and temporary error codes. This ensures the correctness of the error handling behavior across different conditions. --- senderror_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/senderror_test.go b/senderror_test.go index 8584e32..5797520 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -218,6 +218,78 @@ func TestSendError_Msg(t *testing.T) { }) } +func TestSendError_EnhancedStatusCode(t *testing.T) { + t.Run("SendError with no enhanced status code", func(t *testing.T) { + err := &SendError{ + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + } + if err.EnhancedStatusCode() != "" { + t.Errorf("expected empty enhanced status code, got: %s", err.EnhancedStatusCode()) + } + }) + t.Run("SendError with enhanced status code", func(t *testing.T) { + err := &SendError{ + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + enhancedStatusCode: "5.7.1", + } + if err.EnhancedStatusCode() != "5.7.1" { + t.Errorf("expected enhanced status code: %s, got: %s", "5.7.1", err.EnhancedStatusCode()) + } + }) + t.Run("enhanced status code on nil error should return empty string", func(t *testing.T) { + var err *SendError + if err.EnhancedStatusCode() != "" { + t.Error("expected empty enhanced status code on nil-senderror") + } + }) +} + +func TestSendError_ErrorCode(t *testing.T) { + t.Run("ErrorCode with a go-mail error should return 0", func(t *testing.T) { + err := &SendError{ + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + errcode: getErrorCode(ErrNoRcptAddresses), + } + if err.ErrorCode() != 0 { + t.Errorf("expected error code: %d, got: %d", 0, err.ErrorCode()) + } + }) + t.Run("SendError with permanent error", func(t *testing.T) { + err := &SendError{ + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + errcode: getErrorCode(errors.New("535 5.7.8 Error: authentication failed")), + } + if err.ErrorCode() != 535 { + t.Errorf("expected error code: %d, got: %d", 535, err.ErrorCode()) + } + }) + t.Run("SendError with temporary error", func(t *testing.T) { + err := &SendError{ + errlist: []error{ErrNoRcptAddresses}, + rcpt: []string{"", ""}, + Reason: ErrAmbiguous, + errcode: getErrorCode(errors.New("441 4.1.0 Server currently unavailable")), + } + if err.ErrorCode() != 441 { + t.Errorf("expected error code: %d, got: %d", 441, err.ErrorCode()) + } + }) + t.Run("error code on nil error should return 0", func(t *testing.T) { + var err *SendError + if err.ErrorCode() != 0 { + t.Error("expected 0 error code on nil-senderror") + } + }) +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { message := NewMsg() From e8fb977afeb81436bff654c5f992f7adc0fc1f13 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 21:48:24 +0100 Subject: [PATCH 070/125] Add tests for getErrorCode function Introduce a suite of unit tests for the getErrorCode function to validate its behavior with various error types, including go-mail errors, permanent and temporary errors, wrapper errors, non-4xx/5xx errors, and non-3-digit codes. --- senderror_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/senderror_test.go b/senderror_test.go index 5797520..7e538c6 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -6,6 +6,7 @@ package mail import ( "errors" + "fmt" "strings" "testing" ) @@ -290,6 +291,45 @@ func TestSendError_ErrorCode(t *testing.T) { }) } +func TestSendError_getErrorCode(t *testing.T) { + t.Run("getErrorCode with a go-mail error should return 0", func(t *testing.T) { + code := getErrorCode(ErrNoRcptAddresses) + if code != 0 { + t.Errorf("expected error code: %d, got: %d", 0, code) + } + }) + t.Run("getErrorCode with permanent error", func(t *testing.T) { + code := getErrorCode(errors.New("535 5.7.8 Error: authentication failed")) + if code != 535 { + t.Errorf("expected error code: %d, got: %d", 535, code) + } + }) + t.Run("getErrorCode with temporary error", func(t *testing.T) { + code := getErrorCode(errors.New("443 4.1.0 Server currently unavailable")) + if code != 443 { + t.Errorf("expected error code: %d, got: %d", 443, code) + } + }) + t.Run("getErrorCode with wrapper error", func(t *testing.T) { + code := getErrorCode(fmt.Errorf("an error occured: %w", errors.New("443 4.1.0 Server currently unavailable"))) + if code != 443 { + t.Errorf("expected error code: %d, got: %d", 443, code) + } + }) + t.Run("getErrorCode with non-4xx and non-5xx error", func(t *testing.T) { + code := getErrorCode(errors.New("220 2.1.0 This is not an error")) + if code != 0 { + t.Errorf("expected error code: %d, got: %d", 0, code) + } + }) + t.Run("getErrorCode with non 3-digit code", func(t *testing.T) { + code := getErrorCode(errors.New("4xx 4.1.0 The status code is invalid")) + if code != 0 { + t.Errorf("expected error code: %d, got: %d", 0, code) + } + }) +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { message := NewMsg() From 6268acac445150cfab834b293e7e5575ec9f2ab5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 22:47:53 +0100 Subject: [PATCH 071/125] Refactor error handling by renaming functions. Renamed `getErrorCode` to `errorCode` and `getEnhancedStatusCode` to `enhancedStatusCode` for consistency. Updated all references in `client.go` and `senderror.go` accordingly, improving readability and maintaining uniformity across the codebase. --- client.go | 32 ++++++++++++++++---------------- senderror.go | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client.go b/client.go index d64c172..9b3251e 100644 --- a/client.go +++ b/client.go @@ -1201,16 +1201,16 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err != nil { return &SendError{ Reason: ErrGetSender, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } } rcpts, err := message.GetRecipients() if err != nil { return &SendError{ Reason: ErrGetRcpts, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } } @@ -1222,8 +1222,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err = c.smtpClient.Mail(from); err != nil { retError := &SendError{ Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { retError.errlist = append(retError.errlist, resetSendErr) @@ -1242,8 +1242,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { rcptSendErr.errlist = append(rcptSendErr.errlist, err) rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) rcptSendErr.isTemp = isTempError(err) - rcptSendErr.errcode = getErrorCode(err) - rcptSendErr.enhancedStatusCode = getEnhancedStatusCode(err, escSupport) + rcptSendErr.errcode = errorCode(err) + rcptSendErr.enhancedStatusCode = enhancedStatusCode(err, escSupport) hasError = true } } @@ -1257,23 +1257,23 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err != nil { return &SendError{ Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } } _, err = message.WriteTo(writer) if err != nil { return &SendError{ Reason: ErrWriteContent, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } } if err = writer.Close(); err != nil { return &SendError{ Reason: ErrSMTPDataClose, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } } message.isDelivered = true @@ -1281,8 +1281,8 @@ func (c *Client) sendSingleMsg(message *Msg) error { if err = c.Reset(); err != nil { return &SendError{ Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), - affectedMsg: message, errcode: getErrorCode(err), - enhancedStatusCode: getEnhancedStatusCode(err, escSupport), + affectedMsg: message, errcode: errorCode(err), + enhancedStatusCode: enhancedStatusCode(err, escSupport), } } return nil diff --git a/senderror.go b/senderror.go index 93ab6f2..e32d74b 100644 --- a/senderror.go +++ b/senderror.go @@ -265,7 +265,7 @@ func isTempError(err error) bool { return err.Error()[0] == '4' } -func getErrorCode(err error) int { +func errorCode(err error) int { rootErr := errors.Unwrap(err) if rootErr != nil { err = rootErr @@ -282,7 +282,7 @@ func getErrorCode(err error) int { return errcode } -func getEnhancedStatusCode(err error, supported bool) string { +func enhancedStatusCode(err error, supported bool) string { if err == nil || !supported { return "" } From f367db0278db541daa802d4250dbde024def1098 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 22:53:18 +0100 Subject: [PATCH 072/125] Refactor error code functions and add enhanced status code tests Renamed `getErrorCode` function to `errorCode` for consistency. Added new tests for the `enhancedStatusCode` function to validate its behavior with various error scenarios. --- senderror_test.go | 65 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/senderror_test.go b/senderror_test.go index 7e538c6..63d4c73 100644 --- a/senderror_test.go +++ b/senderror_test.go @@ -255,7 +255,7 @@ func TestSendError_ErrorCode(t *testing.T) { errlist: []error{ErrNoRcptAddresses}, rcpt: []string{"", ""}, Reason: ErrAmbiguous, - errcode: getErrorCode(ErrNoRcptAddresses), + errcode: errorCode(ErrNoRcptAddresses), } if err.ErrorCode() != 0 { t.Errorf("expected error code: %d, got: %d", 0, err.ErrorCode()) @@ -266,7 +266,7 @@ func TestSendError_ErrorCode(t *testing.T) { errlist: []error{ErrNoRcptAddresses}, rcpt: []string{"", ""}, Reason: ErrAmbiguous, - errcode: getErrorCode(errors.New("535 5.7.8 Error: authentication failed")), + errcode: errorCode(errors.New("535 5.7.8 Error: authentication failed")), } if err.ErrorCode() != 535 { t.Errorf("expected error code: %d, got: %d", 535, err.ErrorCode()) @@ -277,7 +277,7 @@ func TestSendError_ErrorCode(t *testing.T) { errlist: []error{ErrNoRcptAddresses}, rcpt: []string{"", ""}, Reason: ErrAmbiguous, - errcode: getErrorCode(errors.New("441 4.1.0 Server currently unavailable")), + errcode: errorCode(errors.New("441 4.1.0 Server currently unavailable")), } if err.ErrorCode() != 441 { t.Errorf("expected error code: %d, got: %d", 441, err.ErrorCode()) @@ -291,45 +291,78 @@ func TestSendError_ErrorCode(t *testing.T) { }) } -func TestSendError_getErrorCode(t *testing.T) { - t.Run("getErrorCode with a go-mail error should return 0", func(t *testing.T) { - code := getErrorCode(ErrNoRcptAddresses) +func TestSendError_errorCode(t *testing.T) { + t.Run("errorCode with a go-mail error should return 0", func(t *testing.T) { + code := errorCode(ErrNoRcptAddresses) if code != 0 { t.Errorf("expected error code: %d, got: %d", 0, code) } }) - t.Run("getErrorCode with permanent error", func(t *testing.T) { - code := getErrorCode(errors.New("535 5.7.8 Error: authentication failed")) + t.Run("errorCode with permanent error", func(t *testing.T) { + code := errorCode(errors.New("535 5.7.8 Error: authentication failed")) if code != 535 { t.Errorf("expected error code: %d, got: %d", 535, code) } }) - t.Run("getErrorCode with temporary error", func(t *testing.T) { - code := getErrorCode(errors.New("443 4.1.0 Server currently unavailable")) + t.Run("errorCode with temporary error", func(t *testing.T) { + code := errorCode(errors.New("443 4.1.0 Server currently unavailable")) if code != 443 { t.Errorf("expected error code: %d, got: %d", 443, code) } }) - t.Run("getErrorCode with wrapper error", func(t *testing.T) { - code := getErrorCode(fmt.Errorf("an error occured: %w", errors.New("443 4.1.0 Server currently unavailable"))) + t.Run("errorCode with wrapper error", func(t *testing.T) { + code := errorCode(fmt.Errorf("an error occured: %w", errors.New("443 4.1.0 Server currently unavailable"))) if code != 443 { t.Errorf("expected error code: %d, got: %d", 443, code) } }) - t.Run("getErrorCode with non-4xx and non-5xx error", func(t *testing.T) { - code := getErrorCode(errors.New("220 2.1.0 This is not an error")) + t.Run("errorCode with non-4xx and non-5xx error", func(t *testing.T) { + code := errorCode(errors.New("220 2.1.0 This is not an error")) if code != 0 { t.Errorf("expected error code: %d, got: %d", 0, code) } }) - t.Run("getErrorCode with non 3-digit code", func(t *testing.T) { - code := getErrorCode(errors.New("4xx 4.1.0 The status code is invalid")) + t.Run("errorCode with non 3-digit code", func(t *testing.T) { + code := errorCode(errors.New("4xx 4.1.0 The status code is invalid")) if code != 0 { t.Errorf("expected error code: %d, got: %d", 0, code) } }) } +func TestSendError_enhancedStatusCode(t *testing.T) { + t.Run("enhancedStatusCode with nil error should return empty string", func(t *testing.T) { + code := enhancedStatusCode(nil, true) + if code != "" { + t.Errorf("expected empty enhanced status code, got: %s", code) + } + }) + t.Run("enhancedStatusCode with error but no support should return empty string", func(t *testing.T) { + code := enhancedStatusCode(errors.New("553 5.5.3 something went wrong"), false) + if code != "" { + t.Errorf("expected empty enhanced status code, got: %s", code) + } + }) + t.Run("enhancedStatusCode with error and support", func(t *testing.T) { + code := enhancedStatusCode(errors.New("553 5.5.3 something went wrong"), true) + if code != "5.5.3" { + t.Errorf("expected enhanced status code: %s, got: %s", "5.5.3", code) + } + }) + t.Run("enhancedStatusCode with wrapped error and support", func(t *testing.T) { + code := enhancedStatusCode(fmt.Errorf("this error is wrapped: %w", errors.New("553 5.5.3 something went wrong")), true) + if code != "5.5.3" { + t.Errorf("expected enhanced status code: %s, got: %s", "5.5.3", code) + } + }) + t.Run("enhancedStatusCode with 3xx error", func(t *testing.T) { + code := enhancedStatusCode(errors.New("300 3.0.0 i don't know what i'm doing"), true) + if code != "" { + t.Errorf("expected enhanced status code to be empty, got: %s", code) + } + }) +} + // returnSendError is a helper method to retunr a SendError with a specific reason func returnSendError(r SendErrReason, t bool) error { message := NewMsg() From 719e5b217cbef2ef056c901f769f01eae0483a8f Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 23:02:30 +0100 Subject: [PATCH 073/125] Enhance error handling with ENHANCEDSTATUSCODES check Added a check for the ENHANCEDSTATUSCODES extension and included error code and enhanced status code information in SendError. This helps in providing more detailed error reporting and troubleshooting. --- client_119.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client_119.go b/client_119.go index 093967e..b9931b8 100644 --- a/client_119.go +++ b/client_119.go @@ -27,8 +27,13 @@ import "errors" // - An error that represents the sending result, which may include multiple SendErrors if // any occurred; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) error { + escSupport := false + if c.smtpClient != nil { + escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES") + } 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), + errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport)} } var errs []*SendError for id, message := range messages { From a5ac7c33708413f39ad99001ef01570e65603280 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 13 Nov 2024 23:04:39 +0100 Subject: [PATCH 074/125] Update error handling to include error code and status Previously, only the isTemp flag was considered when aggregating errors. Now, the error code and enhanced status code from the last error are also included. This ensures more comprehensive error reporting and handling. --- client_119.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client_119.go b/client_119.go index b9931b8..5837337 100644 --- a/client_119.go +++ b/client_119.go @@ -55,9 +55,11 @@ func (c *Client) Send(messages ...*Msg) error { returnErr.rcpt = append(returnErr.rcpt, errs[i].rcpt...) } - // We assume that the isTemp flag from the last error we received should be the + // We assume that the error codes and flags from the last error we received should be the // indicator for the returned isTemp flag as well returnErr.isTemp = errs[len(errs)-1].isTemp + returnErr.errcode = errs[len(errs)-1].errcode + returnErr.enhancedStatusCode = errs[len(errs)-1].enhancedStatusCode return returnErr } From c8d7cf86e14642aee524bcfd703bbaca45583208 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 14 Nov 2024 10:17:18 +0100 Subject: [PATCH 075/125] Enhance error handling in Client's Send method Added support for Enhanced Status Codes (ESC) when checking the SMTP client's extensions. The SendError struct now includes the error code and enhanced status code for improved diagnostics. --- client_120.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client_120.go b/client_120.go index 012a4f7..38eb76e 100644 --- a/client_120.go +++ b/client_120.go @@ -27,8 +27,13 @@ import ( // Returns: // - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) (returnErr error) { + escSupport := false + if c.smtpClient != nil { + escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES") + } if err := c.checkConn(); err != nil { - returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err)} + returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), + errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport)} return } From bd655b768b9c44a62f6fc7b61d6e97ac766de86d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 14 Nov 2024 10:20:52 +0100 Subject: [PATCH 076/125] Refactor SendError initialization for better readability Structured the initialization of SendError on connection errors to improve code readability and maintainability. This change affects the error handling in both client_120.go and client_119.go by spreading the error details across multiple lines. --- client_119.go | 6 ++++-- client_120.go | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client_119.go b/client_119.go index 5837337..a28f747 100644 --- a/client_119.go +++ b/client_119.go @@ -32,8 +32,10 @@ func (c *Client) Send(messages ...*Msg) error { escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES") } if err := c.checkConn(); err != nil { - return &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), - errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport)} + return &SendError{ + Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), + errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport), + } } var errs []*SendError for id, message := range messages { diff --git a/client_120.go b/client_120.go index 38eb76e..67c5b5e 100644 --- a/client_120.go +++ b/client_120.go @@ -32,8 +32,10 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES") } if err := c.checkConn(); err != nil { - returnErr = &SendError{Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), - errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport)} + returnErr = &SendError{ + Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), + errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport), + } return } From ca3f50552e283eb2e7b040d2038b7157b7ba11b0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 14 Nov 2024 10:36:24 +0100 Subject: [PATCH 077/125] Allow configuration of test server port via environment variable Moved TestServerPortBase initialization to use an environment variable `TEST_BASEPORT` if provided. This adjustment helps in specifying custom base ports for running tests, ensuring better flexibility in different testing environments. --- client_test.go | 18 ++++++++++++++++-- smtp/smtp_test.go | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/client_test.go b/client_test.go index d1afc86..2ac8811 100644 --- a/client_test.go +++ b/client_test.go @@ -17,6 +17,7 @@ import ( "net/mail" "os" "reflect" + "strconv" "strings" "sync" "sync/atomic" @@ -34,14 +35,15 @@ const ( 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 = 30025 // TestSenderValid is a test sender email address considered valid for sending test emails. TestSenderValid = "valid-from@domain.tld" // TestRcptValid is a test recipient email address considered valid for sending test emails. TestRcptValid = "valid-to@domain.tld" ) +// TestServerPortBase is the base port for the simple SMTP test server +var TestServerPortBase int32 = 30025 + // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. var PortAdder atomic.Int32 @@ -98,6 +100,18 @@ type logData struct { Lines []logLine `json:"lines"` } +func init() { + testPort := os.Getenv("TEST_BASEPORT") + if testPort == "" { + return + } + if port, err := strconv.Atoi(testPort); err == nil { + if port <= 65000 && port > 1023 { + TestServerPortBase = int32(port) + } + } +} + func TestNewClient(t *testing.T) { t.Run("create new Client", func(t *testing.T) { client, err := NewClient(DefaultHost) diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 1b616fc..764d65b 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -30,6 +30,7 @@ import ( "io" "net" "os" + "strconv" "strings" "sync" "sync/atomic" @@ -46,13 +47,14 @@ const ( 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 = 30025 ) // PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances. var PortAdder atomic.Int32 +// TestServerPortBase is the base port for the simple SMTP test server +var TestServerPortBase int32 = 30025 + // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: // // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \ @@ -231,6 +233,18 @@ var authTests = []authTest{ }, } +func init() { + testPort := os.Getenv("TEST_BASEPORT") + if testPort == "" { + return + } + if port, err := strconv.Atoi(testPort); err == nil { + if port <= 65000 && port > 1023 { + TestServerPortBase = int32(port) + } + } +} + func TestAuth(t *testing.T) { t.Run("Auth for all supported auth methods", func(t *testing.T) { for i, tt := range authTests { From a70dde5a4d976808d881e4a62a12620cca6262e2 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 14 Nov 2024 10:41:10 +0100 Subject: [PATCH 078/125] Add TEST_BASEPORT environment variable to CI workflow In the CI configuration file, the TEST_BASEPORT environment variable was added to various job scopes. This ensures consistency and allows the test base port to be set properly across different OS versions and Go versions. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a842e9..7de4077 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }} PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }} + TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} TEST_HOST: ${{ secrets.TEST_HOST }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} @@ -126,6 +127,8 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] go: ['1.19', '1.20', '1.21', '1.22', '1.23'] + env: + TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 @@ -149,6 +152,8 @@ jobs: strategy: matrix: osver: ['14.1', '14.0', 13.4'] + env: + TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} steps: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master @@ -189,6 +194,7 @@ jobs: go: ['1.23'] env: PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} + TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} TEST_HOST: ${{ secrets.TEST_HOST }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} From 2bde3404286584c8caf628297609869a85634203 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 14 Nov 2024 10:45:35 +0100 Subject: [PATCH 079/125] Update SMTP test port variable and CI configuration Changed the SMTP test server base port and updated the corresponding environment variable name to `TEST_BASEPORT_SMTP`. This ensures consistency across the test setup and CI workflow configuration. --- .github/workflows/ci.yml | 4 ++++ smtp/smtp_test.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7de4077..8004942 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: PERFORM_UNIX_OPEN_WRITE_TESTS: ${{ vars.PERFORM_UNIX_OPEN_WRITE_TESTS }} PERFORM_SENDMAIL_TESTS: ${{ vars.PERFORM_SENDMAIL_TESTS }} TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} + TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }} TEST_HOST: ${{ secrets.TEST_HOST }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} @@ -129,6 +130,7 @@ jobs: go: ['1.19', '1.20', '1.21', '1.22', '1.23'] env: TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} + TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }} steps: - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 @@ -154,6 +156,7 @@ jobs: osver: ['14.1', '14.0', 13.4'] env: TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} + TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }} steps: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master @@ -195,6 +198,7 @@ jobs: env: PERFORM_ONLINE_TEST: ${{ vars.PERFORM_ONLINE_TEST }} TEST_BASEPORT: ${{ vars.TEST_BASEPORT }} + TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }} TEST_HOST: ${{ secrets.TEST_HOST }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 764d65b..471b1e1 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -53,7 +53,7 @@ const ( var PortAdder atomic.Int32 // TestServerPortBase is the base port for the simple SMTP test server -var TestServerPortBase int32 = 30025 +var TestServerPortBase int32 = 20025 // localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls: // @@ -234,7 +234,7 @@ var authTests = []authTest{ } func init() { - testPort := os.Getenv("TEST_BASEPORT") + testPort := os.Getenv("TEST_BASEPORT_SMTP") if testPort == "" { return } From 1a811f3bcf609853f81e904d1c9328675a1c7d0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:50:11 +0000 Subject: [PATCH 080/125] Bump fsfe/reuse-action from 4.0.0 to 5.0.0 Bumps [fsfe/reuse-action](https://github.com/fsfe/reuse-action) from 4.0.0 to 5.0.0. - [Release notes](https://github.com/fsfe/reuse-action/releases) - [Commits](https://github.com/fsfe/reuse-action/compare/3ae3c6bdf1257ab19397fab11fd3312144692083...bb774aa972c2a89ff34781233d275075cbddf542) --- updated-dependencies: - dependency-name: fsfe/reuse-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f920b55..08bac30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,7 +184,7 @@ jobs: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master - name: REUSE Compliance Check - uses: fsfe/reuse-action@3ae3c6bdf1257ab19397fab11fd3312144692083 # v4.0.0 + uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0 sonarqube: name: Test with SonarQube review (${{ matrix.os }} / ${{ matrix.go }}) runs-on: ${{ matrix.os }} From 6fbb88239f782fe5b7b1d09a530f10b5dbe1a9d6 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 15 Nov 2024 12:35:15 +0100 Subject: [PATCH 081/125] Enable gosec linter and add exclusion rules Added gosec to the list of enabled linters in `.golangci.toml`. Defined specific exclusion rules to ignore certain false positives and context-specific issues flagged by gosec, ensuring the linter does not impose on intentional code practices. --- .golangci.toml | 64 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/.golangci.toml b/.golangci.toml index 223dc0b..9456df2 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -9,4 +9,66 @@ exclude-dirs = ["examples"] [linters] enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder", - "errname", "errorlint", "gofmt", "gofumpt"] + "errname", "errorlint", "gofmt", "gofumpt", "gosec"] + +[issues] + +## An overflow is impossible here +[[issues.exclude-rules]] +linters = ["gosec"] +path = "random.go" +text = "G115:" + +## These are tests which intentionally do not need any TLS settings +[[issues.exclude-rules]] +linters = ["gosec"] +path = "client_test.go" +text = "G402:" + +## These are tests which intentionally do not need any TLS settings +[[issues.exclude-rules]] +linters = ["gosec"] +path = "smtp/smtp_test.go" +text = "G402:" + +## We do not dictate a TLS minimum version in the smtp package. go-mail +## itself does set sane defaults +[[issues.exclude-rules]] +linters = ["gosec"] +path = "smtp/smtp.go" +text = "G402:" + +## The chance that we write +2 million tests is very low, I think we can +## ignore this for the time being +[[issues.exclude-rules]] +linters = ["gosec"] +path = "client_test.go" +text = "G109:" + +## The chance that we write +2 million tests is very low, I think we can +## ignore this for the time being +[[issues.exclude-rules]] +linters = ["gosec"] +path = "smtp/smtp_test.go" +text = "G109:" + +## We inform the user about the deprecated status of CRAM-MD5 and suggest +## to use SCRAM-SHA instead +[[issues.exclude-rules]] +linters = ["gosec"] +path = "smtp/auth_cram_md5.go" +text = "G501:" + +## Yes, SHA1 is weak, but in the context of SCRAM it is still considered +## secure for specific applications. The user is information about this +## in the documentation +[[issues.exclude-rules]] +linters = ["gosec"] +path = "smtp/auth_scram.go" +text = "G505:" + +## Test code for SCRAM-SHA1. Can be ignored. +[[issues.exclude-rules]] +linters = ["gosec"] +path = "smtp/smtp_test.go" +text = "G505:" From 7210d679db614cb92162f37d526e9e87d7c3f90a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:02:43 +0000 Subject: [PATCH 082/125] Bump codecov/codecov-action from 4.6.0 to 5.0.1 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.6.0 to 5.0.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238...3b1354a6c45db9f1008891f4eafc1a7e94ce1d18) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08bac30..b97b86b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + uses: codecov/codecov-action@3b1354a6c45db9f1008891f4eafc1a7e94ce1d18 # v5.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos lint: From 1cddf5bc766ae347763d1183c3d62d7643a14ad1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:02:51 +0000 Subject: [PATCH 083/125] Bump github/codeql-action from 3.27.3 to 3.27.4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.3 to 3.27.4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/396bb3e45325a47dd9ef434068033c6d5bb0d11a...ea9e4e37992a54ee68a9622e985e60c8e8f12d9f) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9a4b504..6390149 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/autobuild@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -79,4 +79,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index fe9c6b2..65e4b9a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: sarif_file: results.sarif From ac9117dc500ef9057373f019d2b6deeaa4ac61cc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 14:29:34 +0100 Subject: [PATCH 084/125] Add SMTP authentication auto-discovery Implemented a mechanism to automatically discover and select the strongest supported SMTP authentication type. This feature simplifies the authentication process for users and enhances security by prioritizing stronger mechanisms based on server capabilities. Corresponding tests and documentation have been updated. --- auth.go | 20 ++++++++++++++++++++ client.go | 36 +++++++++++++++++++++++++++++++++++- client_test.go | 5 +++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 66254ee..e4adfe6 100644 --- a/auth.go +++ b/auth.go @@ -136,6 +136,21 @@ const ( // // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" + + // SMTPAuthAutoDiscover is a mechanism that dynamically discovers all authentication mechanisms + // supported by the SMTP server and selects the strongest available one. + // + // This type simplifies authentication by automatically negotiating the most secure mechanism + // offered by the server, based on a predefined security ranking. For instance, mechanisms like + // SCRAM-SHA-256(-PLUS) or XOAUTH2 are prioritized over weaker mechanisms such as CRAM-MD5 or PLAIN. + // + // The negotiation process ensures that mechanisms requiring additional capabilities (e.g., + // SCRAM-SHA-X-PLUS with TLS channel binding) are only selected when the necessary prerequisites + // are in place, such as an active TLS-secured connection. + // + // By automating mechanism selection, SMTPAuthAutoDiscover minimizes configuration effort while + // maximizing security and compatibility with a wide range of SMTP servers. + SMTPAuthAutoDiscover SMTPAuthType = "AUTODISCOVER" ) // SMTP Auth related static errors @@ -170,6 +185,11 @@ var ( // ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP // authentication type. ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") + + // ErrNoSupportedAuthDiscovered is returned when the SMTP Auth AutoDiscover process fails to identify + // any supported authentication mechanisms offered by the server. + ErrNoSupportedAuthDiscovered = errors.New("SMTP Auth autodiscover was not able to detect a supported " + + "authentication mechanism") ) // UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type diff --git a/client.go b/client.go index 9b3251e..f92c991 100644 --- a/client.go +++ b/client.go @@ -1100,7 +1100,16 @@ func (c *Client) auth() error { return fmt.Errorf("server does not support SMTP AUTH") } - switch c.smtpAuthType { + authType := c.smtpAuthType + if c.smtpAuthType == SMTPAuthAutoDiscover { + discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) + if err != nil { + return err + } + authType = discoveredType + } + + switch authType { case SMTPAuthPlain: if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { return ErrPlainAuthNotSupported @@ -1172,6 +1181,31 @@ func (c *Client) auth() error { return nil } +func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { + preferList := []SMTPAuthType{SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, + SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin} + if !c.isEncrypted { + preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5} + } + mechs := strings.Split(supported, " ") + + for _, item := range preferList { + if sliceContains(mechs, string(item)) { + return item, nil + } + } + return "", ErrNoSupportedAuthDiscovered +} + +func sliceContains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + // sendSingleMsg sends out a single message and returns an error if the transmission or // delivery fails. It is invoked by the public Send methods. // diff --git a/client_test.go b/client_test.go index 2ac8811..5138c17 100644 --- a/client_test.go +++ b/client_test.go @@ -2304,6 +2304,11 @@ func TestClient_auth(t *testing.T) { name string authType SMTPAuthType }{ + {"LOGIN via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"PLAIN via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"SCRAM-SHA-1 via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"SCRAM-SHA-256 via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"XOAUTH2 via AUTODISCOVER", SMTPAuthAutoDiscover}, {"CRAM-MD5", SMTPAuthCramMD5}, {"LOGIN", SMTPAuthLogin}, {"LOGIN-NOENC", SMTPAuthLoginNoEnc}, From 6d3640a16684765da23052ee7a2e264a70db5992 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:38:29 +0100 Subject: [PATCH 085/125] Fix auth type auto-discovery and add test cases Refactor the auth type initialization to prevent incorrect assignments and handle empty supported lists. Added comprehensive test cases to verify auto-discovery selection of the strongest authentication method and ensure robustness against empty or invalid input. --- client.go | 5 ++++- client_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index f92c991..e61bfad 100644 --- a/client.go +++ b/client.go @@ -1100,7 +1100,7 @@ func (c *Client) auth() error { return fmt.Errorf("server does not support SMTP AUTH") } - authType := c.smtpAuthType + var authType SMTPAuthType if c.smtpAuthType == SMTPAuthAutoDiscover { discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) if err != nil { @@ -1182,6 +1182,9 @@ func (c *Client) auth() error { } func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { + if supported == "" { + return "", ErrNoSupportedAuthDiscovered + } preferList := []SMTPAuthType{SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin} if !c.isEncrypted { diff --git a/client_test.go b/client_test.go index 5138c17..97289b8 100644 --- a/client_test.go +++ b/client_test.go @@ -2514,6 +2514,42 @@ func TestClient_auth(t *testing.T) { }) } +func TestClient_authTypeAutoDiscover(t *testing.T) { + tests := []struct { + supported string + tls bool + expect SMTPAuthType + shouldFail bool + }{ + {"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA256PLUS, false}, + {"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA256, false}, + {"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA1PLUS, false}, + {"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA1, false}, + {"LOGIN XOAUTH2 SCRAM-SHA-1-PLUS", false, SMTPAuthXOAUTH2, false}, + {"PLAIN LOGIN CRAM-MD5", false, SMTPAuthCramMD5, false}, + {"CRAM-MD5", false, SMTPAuthCramMD5, false}, + {"PLAIN", true, SMTPAuthPlain, false}, + {"LOGIN PLAIN", true, SMTPAuthPlain, false}, + {"LOGIN PLAIN", false, "no secure mechanism", true}, + {"", false, "supported list empty", true}, + } + for _, tt := range tests { + t.Run("AutoDiscover selects the strongest auth type: "+string(tt.expect), func(t *testing.T) { + client := &Client{smtpAuthType: SMTPAuthAutoDiscover, isEncrypted: tt.tls} + authType, err := client.authTypeAutoDiscover(tt.supported) + if err != nil && !tt.shouldFail { + t.Fatalf("failed to auto discover auth type: %s", err) + } + if tt.shouldFail && err == nil { + t.Fatal("expected auto discover to fail") + } + if !tt.shouldFail && authType != tt.expect { + t.Errorf("expected strongest auth type: %s, got: %s", tt.expect, authType) + } + }) + } +} + func TestClient_Send(t *testing.T) { message := testMessage(t) t.Run("connect and send email", func(t *testing.T) { From 427e8fd1ed9e7861f7389aac073613f01694643d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:41:32 +0100 Subject: [PATCH 086/125] Format code block consistently Refactor the `preferList` definition in `client.go` for improved readability and consistency. This change ensures the code aligns with standard formatting practices. --- client.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index e61bfad..a611d4c 100644 --- a/client.go +++ b/client.go @@ -1185,8 +1185,10 @@ func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { if supported == "" { return "", ErrNoSupportedAuthDiscovered } - preferList := []SMTPAuthType{SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, - SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin} + preferList := []SMTPAuthType{ + SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, + SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin, + } if !c.isEncrypted { preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5} } From d8df26bbc88536dc2afcab5ab438b12a6eb32627 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:46:46 +0100 Subject: [PATCH 087/125] Fix regression --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index a611d4c..c301f45 100644 --- a/client.go +++ b/client.go @@ -1100,7 +1100,7 @@ func (c *Client) auth() error { return fmt.Errorf("server does not support SMTP AUTH") } - var authType SMTPAuthType + authType := c.smtpAuthType if c.smtpAuthType == SMTPAuthAutoDiscover { discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) if err != nil { From f296f53c1e52efcbab0097e2640f5a9cd8e34440 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:58:58 +0100 Subject: [PATCH 088/125] Add support for SMTP auto-discovery authentication Extended the `UnmarshalString` function in `auth.go` to recognize "auto", "autodiscover", and "autodiscovery" as `SMTPAuthAutoDiscover`. Corresponding test cases were also added to `auth_test.go` to ensure proper functionality. --- auth.go | 2 ++ auth_test.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/auth.go b/auth.go index e4adfe6..3c520ae 100644 --- a/auth.go +++ b/auth.go @@ -196,6 +196,8 @@ var ( // https://pkg.go.dev/github.com/kkyr/fig#StringUnmarshaler func (sa *SMTPAuthType) UnmarshalString(value string) error { switch strings.ToLower(value) { + case "auto", "autodiscover", "autodiscovery": + *sa = SMTPAuthAutoDiscover case "cram-md5", "crammd5", "cram": *sa = SMTPAuthCramMD5 case "custom": diff --git a/auth_test.go b/auth_test.go index a73eaca..a687af3 100644 --- a/auth_test.go +++ b/auth_test.go @@ -12,6 +12,9 @@ func TestSMTPAuthType_UnmarshalString(t *testing.T) { authString string expected SMTPAuthType }{ + {"AUTODISCOVER: auto", "auto", SMTPAuthAutoDiscover}, + {"AUTODISCOVER: autodiscover", "autodiscover", SMTPAuthAutoDiscover}, + {"AUTODISCOVER: autodiscovery", "autodiscovery", SMTPAuthAutoDiscover}, {"CRAM-MD5: cram-md5", "cram-md5", SMTPAuthCramMD5}, {"CRAM-MD5: crammd5", "crammd5", SMTPAuthCramMD5}, {"CRAM-MD5: cram", "cram", SMTPAuthCramMD5}, From 95ae33255f35118edb54806862adeee415a09449 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:46:16 +0000 Subject: [PATCH 089/125] Bump codecov/codecov-action from 5.0.1 to 5.0.2 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/3b1354a6c45db9f1008891f4eafc1a7e94ce1d18...5c47607acb93fed5485fdbf7232e8a31425f672a) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b97b86b..e867e57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() - uses: codecov/codecov-action@3b1354a6c45db9f1008891f4eafc1a7e94ce1d18 # v5.0.1 + uses: codecov/codecov-action@5c47607acb93fed5485fdbf7232e8a31425f672a # v5.0.2 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos lint: From b137fe46116ffd9ca7a390440a1abac36d46cc97 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 18 Nov 2024 16:44:00 +0100 Subject: [PATCH 090/125] Refactor file handling to use io/fs interface Updated functions to use the io/fs package instead of embed.FS, making the code more flexible with respect to different filesystem implementations. Revised the method signatures and related documentation to reflect this change. --- msg.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/msg.go b/msg.go index 0b06a22..0a9f075 100644 --- a/msg.go +++ b/msg.go @@ -12,6 +12,7 @@ import ( "fmt" ht "html/template" "io" + "io/fs" "mime" "net/mail" "os" @@ -1964,7 +1965,7 @@ func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) e if fs == nil { return fmt.Errorf("embed.FS must not be nil") } - file, err := fileFromEmbedFS(name, fs) + file, err := fileFromIOFS(name, fs) if err != nil { return err } @@ -2110,7 +2111,7 @@ func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) er if fs == nil { return fmt.Errorf("embed.FS must not be nil") } - file, err := fileFromEmbedFS(name, fs) + file, err := fileFromIOFS(name, fs) if err != nil { return err } @@ -2666,15 +2667,15 @@ func (m *Msg) addDefaultHeader() { m.SetGenHeader(HeaderMIMEVersion, string(m.mimever)) } -// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS. +// fileFromIOFS returns a File pointer from a given file in the provided fs.FS. // -// This method retrieves a file from the embedded filesystem (embed.FS) and returns a File structure +// This method retrieves a file from the provided io/fs (fs.FS) and returns a File structure // that can be used as an attachment or embed in the email message. The file's content is read when // writing to an io.Writer, and the file is identified by its base name. // // Parameters: // - name: The name of the file to retrieve from the embedded filesystem. -// - fs: A pointer to the embed.FS from which the file will be opened. +// - fs: An instance that satisfies the fs.FS interface // // Returns: // - A pointer to the File structure representing the embedded file. @@ -2682,8 +2683,8 @@ func (m *Msg) addDefaultHeader() { // // References: // - https://datatracker.ietf.org/doc/html/rfc2183 -func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { - _, err := fs.Open(name) +func fileFromIOFS(name string, iofs fs.FS) (*File, error) { + _, err := iofs.Open(name) if err != nil { return nil, fmt.Errorf("failed to open file from embed.FS: %w", err) } @@ -2691,7 +2692,7 @@ func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) { Name: filepath.Base(name), Header: make(map[string][]string), Writer: func(writer io.Writer) (int64, error) { - file, err := fs.Open(name) + file, err := iofs.Open(name) if err != nil { return 0, err } From 101a35f7d39ebf81626fae59c80cbfd9cf616e82 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 19 Nov 2024 10:52:54 +0100 Subject: [PATCH 091/125] Add AttachFromIOFS and EmbedFromIOFS functions Introduce new methods AttachFromIOFS and EmbedFromIOFS to handle attachments and embeds from a general file system (fs.FS). Updated tests to cover these new functionalities and modified error messages for consistency. Updated README to reflect support for fs.FS. --- README.md | 2 +- msg.go | 64 +++++++++++++++++++++----- msg_test.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 947b6f2..9f17180 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Here are some highlights of go-mail's featureset: * [X] RFC5322 compliant mail address validation * [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.) * [X] Concurrency-safe reusing the same SMTP connection to send multiple mails -* [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`, `embed.FS` or `fs.FS`) * [X] Support for different encodings * [X] Middleware support for 3rd-party libraries to alter mail messages * [X] Support sending mails via a local sendmail command diff --git a/msg.go b/msg.go index 0a9f075..762ec2c 100644 --- a/msg.go +++ b/msg.go @@ -1963,9 +1963,28 @@ func (m *Msg) AttachTextTemplate( // - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { if fs == nil { - return fmt.Errorf("embed.FS must not be nil") + return errors.New("embed.FS must not be nil") } - file, err := fileFromIOFS(name, fs) + return m.AttachFromIOFS(name, *fs, opts...) +} + +// AttachFromIOFS attaches a file from a generic file system to the message. +// +// This function retrieves a file by name from an fs.FS instance, processes it, and appends it to the +// message's attachment collection. Additional file options can be provided for further customization. +// +// Parameters: +// - name: The name of the file to retrieve from the file system. +// - iofs: The file system (must not be nil). +// - opts: Optional file options to customize the attachment process. +// +// Returns: +// - An error if the file cannot be retrieved, the fs.FS is nil, or any other issue occurs. +func (m *Msg) AttachFromIOFS(name string, iofs fs.FS, opts ...FileOption) error { + if iofs == nil { + return errors.New("fs.FS must not be nil") + } + file, err := fileFromIOFS(name, iofs) if err != nil { return err } @@ -2109,9 +2128,28 @@ func (m *Msg) EmbedTextTemplate( // - https://datatracker.ietf.org/doc/html/rfc2183 func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error { if fs == nil { - return fmt.Errorf("embed.FS must not be nil") + return errors.New("embed.FS must not be nil") } - file, err := fileFromIOFS(name, fs) + return m.EmbedFromIOFS(name, *fs, opts...) +} + +// EmbedFromIOFS embeds a file from a generic file system into the message. +// +// This function retrieves a file by name from an fs.FS instance, processes it, and appends it to the +// message's embed collection. Additional file options can be provided for further customization. +// +// Parameters: +// - name: The name of the file to retrieve from the file system. +// - iofs: The file system (must not be nil). +// - opts: Optional file options to customize the embedding process. +// +// Returns: +// - An error if the file cannot be retrieved, the fs.FS is nil, or any other issue occurs. +func (m *Msg) EmbedFromIOFS(name string, iofs fs.FS, opts ...FileOption) error { + if iofs == nil { + return errors.New("fs.FS must not be nil") + } + file, err := fileFromIOFS(name, iofs) if err != nil { return err } @@ -2684,22 +2722,26 @@ func (m *Msg) addDefaultHeader() { // References: // - https://datatracker.ietf.org/doc/html/rfc2183 func fileFromIOFS(name string, iofs fs.FS) (*File, error) { + if iofs == nil { + return nil, errors.New("fs.FS is nil") + } + _, err := iofs.Open(name) if err != nil { - return nil, fmt.Errorf("failed to open file from embed.FS: %w", err) + return nil, fmt.Errorf("failed to open file from fs.FS: %w", err) } return &File{ Name: filepath.Base(name), Header: make(map[string][]string), Writer: func(writer io.Writer) (int64, error) { - file, err := iofs.Open(name) - if err != nil { - return 0, err + file, ferr := iofs.Open(name) + if ferr != nil { + return 0, fmt.Errorf("failed to open file from fs.FS: %w", ferr) } - numBytes, err := io.Copy(writer, file) - if err != nil { + numBytes, ferr := io.Copy(writer, file) + if ferr != nil { _ = file.Close() - return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err) + return numBytes, fmt.Errorf("failed to copy file from fs.FS to io.Writer: %w", ferr) } return numBytes, file.Close() }, diff --git a/msg_test.go b/msg_test.go index 3f2cf94..ac1a32a 100644 --- a/msg_test.go +++ b/msg_test.go @@ -4970,6 +4970,75 @@ func TestMsg_AttachFromEmbedFS(t *testing.T) { }) } +func TestMsg_AttachFromIOFS(t *testing.T) { + t.Run("AttachFromIOFS successful", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.AttachFromIOFS("testdata/attachment.txt", efs, + WithFileName("attachment.txt")); err != nil { + t.Fatalf("failed to attach from embed FS: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to retrieve attachments list") + } + if attachments[0] == nil { + t.Fatal("expected attachment to be not nil") + } + if attachments[0].Name != "attachment.txt" { + t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := attachments[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test attachment") { + t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got) + } + }) + t.Run("AttachFromIOFS with invalid path", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.AttachFromIOFS("testdata/invalid.txt", efs, WithFileName("attachment.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + t.Run("AttachFromIOFS with nil embed FS", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.AttachFromIOFS("testdata/invalid.txt", nil, WithFileName("attachment.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + t.Run("AttachFromIOFS with fs.FS fails on copy", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.AttachFromIOFS("testdata/attachment.txt", efs); err != nil { + t.Fatalf("failed to attach file from fs.FS: %s", err) + } + attachments := message.GetAttachments() + if len(attachments) != 1 { + t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments)) + } + _, err := attachments[0].Writer(failReadWriteSeekCloser{}) + if err == nil { + t.Error("writer func expected to fail, but didn't") + } + }) +} + func TestMsg_EmbedFile(t *testing.T) { t.Run("EmbedFile with file", func(t *testing.T) { message := NewMsg() @@ -5435,6 +5504,58 @@ func TestMsg_EmbedFromEmbedFS(t *testing.T) { }) } +func TestMsg_EmbedFromIOFS(t *testing.T) { + t.Run("EmbedFromIOFS successful", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + if err := message.EmbedFromIOFS("testdata/embed.txt", efs, + WithFileName("embed.txt")); err != nil { + t.Fatalf("failed to embed from embed FS: %s", err) + } + embeds := message.GetEmbeds() + if len(embeds) != 1 { + t.Fatalf("failed to retrieve embeds list") + } + if embeds[0] == nil { + t.Fatal("expected embed to be not nil") + } + if embeds[0].Name != "embed.txt" { + t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name) + } + messageBuf := bytes.NewBuffer(nil) + _, err := embeds[0].Writer(messageBuf) + if err != nil { + t.Errorf("writer func failed: %s", err) + } + got := strings.TrimSpace(messageBuf.String()) + if !strings.EqualFold(got, "This is a test embed") { + t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got) + } + }) + t.Run("EmbedFromIOFS with invalid path", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.EmbedFromIOFS("testdata/invalid.txt", efs, WithFileName("embed.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) + t.Run("EmbedFromIOFS with nil embed FS", func(t *testing.T) { + message := NewMsg() + if message == nil { + t.Fatal("message is nil") + } + err := message.EmbedFromIOFS("testdata/invalid.txt", nil, WithFileName("embed.txt")) + if err == nil { + t.Fatal("expected error, got nil") + } + }) +} + func TestMsg_Reset(t *testing.T) { message := NewMsg() if message == nil { @@ -6440,6 +6561,15 @@ func TestMsg_addDefaultHeader(t *testing.T) { }) } +func TestMsg_fileFromIOFS(t *testing.T) { + t.Run("file from fs.FS where fs is nil ", func(t *testing.T) { + _, err := fileFromIOFS("testfile.txt", nil) + if err == nil { + t.Fatal("expected error for fs.FS that is nil") + } + }) +} + // uppercaseMiddleware is a middleware type that transforms the subject to uppercase. type uppercaseMiddleware struct{} From 93fc646338143b533cf9065ff67e48581c7bf0a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:32:14 +0000 Subject: [PATCH 092/125] Bump step-security/harden-runner from 2.10.1 to 2.10.2 Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.10.1 to 2.10.2. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/91182cccc01eb5e619899d80e4e971d6181294a7...0080882f6c36860b6ba35c610c98ce87d4e2f26f) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/scorecards.yml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e867e57..2756f0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: TEST_PASS: ${{ secrets.TEST_PASS }} steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Checkout Code @@ -73,7 +73,7 @@ jobs: go: ['1.23'] steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Setup go @@ -95,7 +95,7 @@ jobs: cancel-in-progress: true steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Checkout Code @@ -113,7 +113,7 @@ jobs: cancel-in-progress: true steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Run govulncheck @@ -133,7 +133,7 @@ jobs: TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }} steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Checkout Code @@ -178,7 +178,7 @@ jobs: cancel-in-progress: true steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Checkout Code @@ -204,7 +204,7 @@ jobs: TEST_PASS: ${{ secrets.TEST_PASS }} steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit - name: Checkout Code diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6390149..3047e7b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 65e4b9a..4b242b4 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 with: egress-policy: audit From d30a4a73c6d8c12e8d900bcc76f5466555d48616 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 19 Nov 2024 17:29:17 +0100 Subject: [PATCH 093/125] Add QuickSend function and unit tests Introduce the QuickSend function for sending emails quickly with TLS and optional SMTP authentication. Added comprehensive unit tests to ensure QuickSend works correctly with different authentication mechanisms and handles various error scenarios. --- quicksend.go | 112 ++++++++++++++ quicksend_test.go | 368 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 quicksend.go create mode 100644 quicksend_test.go diff --git a/quicksend.go b/quicksend.go new file mode 100644 index 0000000..797b4bd --- /dev/null +++ b/quicksend.go @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "bytes" + "crypto/tls" + "fmt" + "net" + "strconv" +) + +type AuthData struct { + Auth bool + Username string + Password string +} + +var testHookTLSConfig func() *tls.Config // nil, except for tests + +// QuickSend is an all-in-one method for quickly sending simple text mails in go-mail. +// +// This method will create a new client that connects to the server at addr, switches to TLS if possible, +// authenticates with the optional AuthData provided in auth and create a new simple Msg with the provided +// subject string and message bytes as body. The message will be sent using from as sender address and will +// be delivered to every address in rcpts. QuickSend will always send as text/plain ContentType. +// +// For the SMTP authentication, if auth is not nil and AuthData.Auth is set to true, it will try to +// autodiscover the best SMTP authentication mechanism supported by the server. If auth is set to true +// but autodiscover is not able to find a suitable authentication mechanism or if the authentication +// fails, the mail delivery will fail completely. +// +// The content parameter should be an RFC 822-style email body. The lines of content should be CRLF terminated. +// +// Parameters: +// - addr: The hostname and port of the mail server, it must include a port, as in "mail.example.com:smtp". +// - auth: A AuthData pointer. If nil or if AuthData.Auth is set to false, not SMTP authentication will be performed. +// - from: The from address of the sender as string. +// - rcpts: A slice of strings of receipient addresses. +// - subject: The subject line as string. +// - content: A byte slice of the mail content +// - opts: Optional parameters for customizing the body part. +// +// Returns: +// - A pointer to the generated Msg. +// - An error if any step in the process of mail generation or delivery failed. +func QuickSend(addr string, auth *AuthData, from string, rcpts []string, subject string, content []byte) (*Msg, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("failed to split host and port from address: %w", err) + } + portnum, err := strconv.Atoi(port) + if err != nil { + return nil, fmt.Errorf("failed to convert port to int: %w", err) + } + client, err := NewClient(host, WithPort(portnum), WithTLSPolicy(TLSOpportunistic)) + if err != nil { + return nil, fmt.Errorf("failed to create new client: %w", err) + } + + if auth != nil && auth.Auth { + client.SetSMTPAuth(SMTPAuthAutoDiscover) + client.SetUsername(auth.Username) + client.SetPassword(auth.Password) + } + + tlsConfig := client.tlsconfig + if testHookTLSConfig != nil { + tlsConfig = testHookTLSConfig() + } + if err = client.SetTLSConfig(tlsConfig); err != nil { + return nil, fmt.Errorf("failed to set TLS config: %w", err) + } + + message := NewMsg() + if err = message.From(from); err != nil { + return nil, fmt.Errorf("failed to set MAIL FROM address: %w", err) + } + if err = message.To(rcpts...); err != nil { + return nil, fmt.Errorf("failed to set RCPT TO address: %w", err) + } + message.Subject(subject) + buffer := bytes.NewBuffer(content) + writeFunc := writeFuncFromBuffer(buffer) + message.SetBodyWriter(TypeTextPlain, writeFunc) + + if err = client.DialAndSend(message); err != nil { + return nil, fmt.Errorf("failed to dial and send message: %w", err) + } + return message, nil +} + +// NewAuthData creates a new AuthData instance with the provided username and password. +// +// This function initializes an AuthData struct with authentication enabled and sets the +// username and password fields. +// +// Parameters: +// - user: The username for authentication. +// - pass: The password for authentication. +// +// Returns: +// - A pointer to the initialized AuthData instance. +func NewAuthData(user, pass string) *AuthData { + return &AuthData{ + Auth: true, + Username: user, + Password: pass, + } +} diff --git a/quicksend_test.go b/quicksend_test.go new file mode 100644 index 0000000..497e2a0 --- /dev/null +++ b/quicksend_test.go @@ -0,0 +1,368 @@ +// SPDX-FileCopyrightText: 2024 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "strings" + "testing" + "time" +) + +func TestNewAuthData(t *testing.T) { + t.Run("AuthData with username and password", func(t *testing.T) { + auth := NewAuthData("username", "password") + if !auth.Auth { + t.Fatal("expected auth to be true") + } + if auth.Username != "username" { + t.Fatalf("expected username to be %s, got %s", "username", auth.Username) + } + if auth.Password != "password" { + t.Fatalf("expected password to be %s, got %s", "password", auth.Password) + } + }) + t.Run("AuthData with username and empty password", func(t *testing.T) { + auth := NewAuthData("username", "") + if !auth.Auth { + t.Fatal("expected auth to be true") + } + if auth.Username != "username" { + t.Fatalf("expected username to be %s, got %s", "username", auth.Username) + } + if auth.Password != "" { + t.Fatalf("expected password to be %s, got %s", "", auth.Password) + } + }) + t.Run("AuthData with empty username and set password", func(t *testing.T) { + auth := NewAuthData("", "password") + if !auth.Auth { + t.Fatal("expected auth to be true") + } + if auth.Username != "" { + t.Fatalf("expected username to be %s, got %s", "", auth.Username) + } + if auth.Password != "password" { + t.Fatalf("expected password to be %s, got %s", "password", auth.Password) + } + }) + t.Run("AuthData with empty data", func(t *testing.T) { + auth := NewAuthData("", "") + if !auth.Auth { + t.Fatal("expected auth to be true") + } + if auth.Username != "" { + t.Fatalf("expected username to be %s, got %s", "", auth.Username) + } + if auth.Password != "" { + t.Fatalf("expected password to be %s, got %s", "", auth.Password) + } + }) +} + +func TestQuickSend(t *testing.T) { + subject := "This is a test subject" + body := []byte("This is a test body\r\nWith multiple lines\r\n\r\nBest,\r\n The go-mail team") + sender := TestSenderValid + rcpts := []string{TestRcptValid} + t.Run("QuickSend with authentication and TLS", func(t *testing.T) { + ctxAuth, cancelAuth := context.WithCancel(context.Background()) + defer cancelAuth() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + echoBuffer := bytes.NewBuffer(nil) + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctxAuth, t, props); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := TestServerAddr + ":" + fmt.Sprint(serverPort) + testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } + + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err != nil { + t.Fatalf("failed to send email: %s", err) + } + + props.BufferMutex.RLock() + resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() + + expects := []struct { + line int + data string + }{ + {8, "STARTTLS"}, + {17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"}, + {21, "MAIL FROM: BODY=8BITMIME SMTPUTF8"}, + {23, "RCPT TO:"}, + {30, "Subject: " + subject}, + {33, "From: "}, + {34, "To: "}, + {35, "Content-Type: text/plain; charset=UTF-8"}, + {36, "Content-Transfer-Encoding: quoted-printable"}, + {38, "This is a test body"}, + {39, "With multiple lines"}, + {40, ""}, + {41, "Best,"}, + {42, " The go-mail team"}, + } + for _, expect := range expects { + if !strings.EqualFold(resp[expect.line], expect.data) { + t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line]) + } + } + }) + t.Run("QuickSend with authentication and TLS and multiple receipients", func(t *testing.T) { + ctxAuth, cancelAuth := context.WithCancel(context.Background()) + defer cancelAuth() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + echoBuffer := bytes.NewBuffer(nil) + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctxAuth, t, props); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := TestServerAddr + ":" + fmt.Sprint(serverPort) + testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } + + multiRcpts := []string{TestRcptValid, TestRcptValid, TestRcptValid} + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, multiRcpts, subject, body) + if err != nil { + t.Fatalf("failed to send email: %s", err) + } + + props.BufferMutex.RLock() + resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() + + expects := []struct { + line int + data string + }{ + {8, "STARTTLS"}, + {17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk"}, + {21, "MAIL FROM: BODY=8BITMIME SMTPUTF8"}, + {23, "RCPT TO:"}, + {25, "RCPT TO:"}, + {27, "RCPT TO:"}, + {34, "Subject: " + subject}, + {37, "From: "}, + {38, "To: , , "}, + {39, "Content-Type: text/plain; charset=UTF-8"}, + {40, "Content-Transfer-Encoding: quoted-printable"}, + {42, "This is a test body"}, + {43, "With multiple lines"}, + {44, ""}, + {45, "Best,"}, + {46, " The go-mail team"}, + } + for _, expect := range expects { + if !strings.EqualFold(resp[expect.line], expect.data) { + t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line]) + } + } + }) + t.Run("QuickSend uses stronged authentication method", func(t *testing.T) { + ctxAuth, cancelAuth := context.WithCancel(context.Background()) + defer cancelAuth() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250-STARTTLS\r\n250 SMTPUTF8" + echoBuffer := bytes.NewBuffer(nil) + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctxAuth, t, props); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := TestServerAddr + ":" + fmt.Sprint(serverPort) + testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } + + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err != nil { + t.Fatalf("failed to send email: %s", err) + } + + props.BufferMutex.RLock() + resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() + + expects := []struct { + line int + data string + }{ + {17, "AUTH SCRAM-SHA-256-PLUS"}, + } + for _, expect := range expects { + if !strings.EqualFold(resp[expect.line], expect.data) { + t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line]) + } + } + }) + t.Run("QuickSend uses stronged authentication method without TLS", func(t *testing.T) { + ctxAuth, cancelAuth := context.WithCancel(context.Background()) + defer cancelAuth() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + echoBuffer := bytes.NewBuffer(nil) + props := &serverProps{ + EchoBuffer: echoBuffer, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctxAuth, t, props); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := TestServerAddr + ":" + fmt.Sprint(serverPort) + testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } + + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err != nil { + t.Fatalf("failed to send email: %s", err) + } + + props.BufferMutex.RLock() + resp := strings.Split(echoBuffer.String(), "\r\n") + props.BufferMutex.RUnlock() + + expects := []struct { + line int + data string + }{ + {7, "AUTH SCRAM-SHA-256"}, + } + for _, expect := range expects { + if !strings.EqualFold(resp[expect.line], expect.data) { + t.Errorf("expected %q at line %d, got: %q", expect.data, expect.line, resp[expect.line]) + } + } + }) + t.Run("QuickSend fails during DialAndSned", func(t *testing.T) { + ctxAuth, cancelAuth := context.WithCancel(context.Background()) + defer cancelAuth() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-AUTH PLAIN CRAM-MD5 SCRAM-SHA-256-PLUS SCRAM-SHA-256\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + props := &serverProps{ + FailOnMailFrom: true, + FeatureSet: featureSet, + ListenPort: serverPort, + } + go func() { + if err := simpleSMTPServer(ctxAuth, t, props); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + addr := TestServerAddr + ":" + fmt.Sprint(serverPort) + testHookTLSConfig = func() *tls.Config { return &tls.Config{InsecureSkipVerify: true} } + + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err == nil { + t.Error("expected QuickSend to fail during DialAndSend") + } + expect := `failed to dial and send message: send failed: sending SMTP MAIL FROM command: 500 ` + + `5.5.2 Error: fail on MAIL FROM` + if !strings.EqualFold(err.Error(), expect) { + t.Errorf("expected error to contain %s, got %s", expect, err) + } + }) + t.Run("QuickSend fails on server address without port", func(t *testing.T) { + addr := TestServerAddr + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err == nil { + t.Error("expected QuickSend to fail with invalid server address") + } + expect := "failed to split host and port from address: address 127.0.0.1: missing port in address" + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected error to contain %s, got %s", expect, err) + } + }) + t.Run("QuickSend fails on server address with invalid port", func(t *testing.T) { + addr := TestServerAddr + ":invalid" + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err == nil { + t.Error("expected QuickSend to fail with invalid server port") + } + expect := `failed to convert port to int: strconv.Atoi: parsing "invalid": invalid syntax` + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected error to contain %s, got %s", expect, err) + } + }) + t.Run("QuickSend fails on nil TLS config (test hook only)", func(t *testing.T) { + addr := TestServerAddr + ":587" + testHookTLSConfig = func() *tls.Config { return nil } + defer func() { + testHookTLSConfig = nil + }() + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, rcpts, subject, body) + if err == nil { + t.Error("expected QuickSend to fail with nil-tlsConfig") + } + expect := `failed to set TLS config: invalid TLS config` + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected error to contain %s, got %s", expect, err) + } + }) + t.Run("QuickSend fails with invalid from address", func(t *testing.T) { + addr := TestServerAddr + ":587" + invalid := "invalid-fromdomain.tld" + _, err := QuickSend(addr, NewAuthData("username", "password"), invalid, rcpts, subject, body) + if err == nil { + t.Error("expected QuickSend to fail with invalid from address") + } + expect := `failed to set MAIL FROM address: failed to parse mail address "invalid-fromdomain.tld": ` + + `mail: missing '@' or angle-addr` + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected error to contain %s, got %s", expect, err) + } + }) + t.Run("QuickSend fails with invalid from address", func(t *testing.T) { + addr := TestServerAddr + ":587" + invalid := []string{"invalid-todomain.tld"} + _, err := QuickSend(addr, NewAuthData("username", "password"), sender, invalid, subject, body) + if err == nil { + t.Error("expected QuickSend to fail with invalid to address") + } + expect := `failed to set RCPT TO address: failed to parse mail address "invalid-todomain.tld": ` + + `mail: missing '@' or angle-add` + if !strings.Contains(err.Error(), expect) { + t.Errorf("expected error to contain %s, got %s", expect, err) + } + }) +} From 7ee4e47c8ec4691bf84ecdac7f70263a047f69ca Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 19 Nov 2024 17:29:26 +0100 Subject: [PATCH 094/125] Add EchoBuffer to serverProps for capturing SMTP data Introduced a new io.Writer field `EchoBuffer` and its associated `BufferMutex` to `serverProps`. Updated relevant test code to write SMTP transaction data to `EchoBuffer` if it is set, ensuring thread safety with `BufferMutex`. --- client_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/client_test.go b/client_test.go index 97289b8..a6530f0 100644 --- a/client_test.go +++ b/client_test.go @@ -3665,6 +3665,8 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", " // serverProps represents the configuration properties for the SMTP server. type serverProps struct { + BufferMutex sync.RWMutex + EchoBuffer io.Writer FailOnAuth bool FailOnDataInit bool FailOnDataClose bool @@ -3754,6 +3756,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server if err != nil { t.Logf("failed to write line: %s", err) } + if props.EchoBuffer != nil { + props.BufferMutex.Lock() + if _, berr := props.EchoBuffer.Write([]byte(data + "\r\n")); berr != nil { + t.Errorf("failed write to echo buffer: %s", berr) + } + props.BufferMutex.Unlock() + } _ = writer.Flush() } writeOK := func() { @@ -3770,6 +3779,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server break } time.Sleep(time.Millisecond) + if props.EchoBuffer != nil { + props.BufferMutex.Lock() + if _, berr := props.EchoBuffer.Write([]byte(data)); berr != nil { + t.Errorf("failed write to echo buffer: %s", berr) + } + props.BufferMutex.Unlock() + } var datastring string data = strings.TrimSpace(data) @@ -3830,6 +3846,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server t.Logf("failed to read data from connection: %s", derr) break } + if props.EchoBuffer != nil { + props.BufferMutex.Lock() + if _, berr := props.EchoBuffer.Write([]byte(ddata)); berr != nil { + t.Errorf("failed write to echo buffer: %s", berr) + } + props.BufferMutex.Unlock() + } ddata = strings.TrimSpace(ddata) if ddata == "." { if props.FailOnDataClose { From f05654d5e58c558253c201b785b65f20dd1a89ab Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 19 Nov 2024 17:29:37 +0100 Subject: [PATCH 095/125] Add exclusion rule for TLS settings in tests Updated the .golangci.toml config to exclude gosec rule G402 in quicksend_test.go. This exclusion is intentional as these tests do not require TLS settings. --- .golangci.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.golangci.toml b/.golangci.toml index 9456df2..5178a27 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -72,3 +72,10 @@ text = "G505:" linters = ["gosec"] path = "smtp/smtp_test.go" text = "G505:" + +## These are tests which intentionally do not need any TLS settings +[[issues.exclude-rules]] +linters = ["gosec"] +path = "quicksend_test.go" +text = "G402:" + From b1a294d3649d4f485eafec79e024dcaf2feaacd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:12:42 +0000 Subject: [PATCH 096/125] Bump codecov/codecov-action from 5.0.2 to 5.0.4 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.2 to 5.0.4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/5c47607acb93fed5485fdbf7232e8a31425f672a...985343d70564a82044c1b7fcb84c2fa05405c1a2) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2756f0f..02be627 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() - uses: codecov/codecov-action@5c47607acb93fed5485fdbf7232e8a31425f672a # v5.0.2 + uses: codecov/codecov-action@985343d70564a82044c1b7fcb84c2fa05405c1a2 # v5.0.4 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos lint: From 1a579c214925ca77c225dfd92f5fb5b41f8811a9 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 21 Nov 2024 10:26:48 +0100 Subject: [PATCH 097/125] Add mutex for concurrent send protection Introduced a sendMutex to synchronize access to shared resources in the DialAndSendWithContext method. This ensures thread safety when sending multiple messages concurrently. Added a corresponding test to verify the concurrent sending functionality. --- client.go | 5 +++++ client_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/client.go b/client.go index c301f45..a7dead8 100644 --- a/client.go +++ b/client.go @@ -170,6 +170,9 @@ type ( // requestDSN indicates wether we want to request DSN (Delivery Status Notifications). requestDSN bool + // sendMutex is used to synchronize access to shared resources during the dial and send methods. + sendMutex sync.Mutex + // smtpAuth is the authentication type that is used to authenticate the user with SMTP server. It // satisfies the smtp.Auth interface. // @@ -1058,6 +1061,8 @@ func (c *Client) DialAndSend(messages ...*Msg) error { // - An error if the connection fails, if sending the messages fails, or if closing the // connection fails; otherwise, returns nil. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { + c.sendMutex.Lock() + defer c.sendMutex.Unlock() if err := c.DialWithContext(ctx); err != nil { return fmt.Errorf("dial failed: %w", err) } diff --git a/client_test.go b/client_test.go index a6530f0..d1912f4 100644 --- a/client_test.go +++ b/client_test.go @@ -2297,6 +2297,45 @@ func TestClient_DialAndSendWithContext(t *testing.T) { t.Errorf("client was supposed to fail on dial") } }) + t.Run("concurrent sending via DialAndSendWithContext", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + + wg := sync.WaitGroup{} + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + msg := testMessage(t) + msg.SetMessageIDWithValue("this.is.a.message.id") + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Second*5) + defer cancelDial() + if goroutineErr := client.DialAndSendWithContext(ctxDial, msg); goroutineErr != nil { + t.Errorf("failed to dial and send message: %s", goroutineErr) + } + }() + } + wg.Wait() + }) } func TestClient_auth(t *testing.T) { From 6e9df0b724bb4be71f520c037c02e49bfe5ed31e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Thu, 21 Nov 2024 10:33:24 +0100 Subject: [PATCH 098/125] Increase timeout for DialAndSend context Updated the timeout for the DialAndSend context in 'client_test.go' from 5 seconds to 1 minute to ensure sufficient time for the operation to complete. This change helps prevent premature timeouts that can cause test failures. --- client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index d1912f4..f6d5161 100644 --- a/client_test.go +++ b/client_test.go @@ -2327,7 +2327,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { msg := testMessage(t) msg.SetMessageIDWithValue("this.is.a.message.id") - ctxDial, cancelDial := context.WithTimeout(ctx, time.Second*5) + ctxDial, cancelDial := context.WithTimeout(ctx, time.Minute) defer cancelDial() if goroutineErr := client.DialAndSendWithContext(ctxDial, msg); goroutineErr != nil { t.Errorf("failed to dial and send message: %s", goroutineErr) From e17965a891bd8faf6673977d7b55fd45e3e3b7f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:02:01 +0000 Subject: [PATCH 099/125] Bump actions/dependency-review-action from 4.4.0 to 4.5.0 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/4081bf99e2866ebe428fc0477b69eb4fcda7220a...3b139cfc5fae8b618d3eae3675e383bb1769c019) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02be627..5818462 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,7 +101,7 @@ jobs: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master - name: 'Dependency Review' - uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0 + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 with: base-ref: ${{ github.event.pull_request.base.sha || 'main' }} head-ref: ${{ github.event.pull_request.head.sha || github.ref }} From 49606f197bd6db2e849c13543752d2367dcadc80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:02:09 +0000 Subject: [PATCH 100/125] Bump codecov/codecov-action from 5.0.4 to 5.0.7 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.4 to 5.0.7. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/985343d70564a82044c1b7fcb84c2fa05405c1a2...015f24e6818733317a2da2edd6290ab26238649a) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02be627..f22632e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() - uses: codecov/codecov-action@985343d70564a82044c1b7fcb84c2fa05405c1a2 # v5.0.4 + uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos lint: From 62b3314c20be1a21f73067c223dce0f138993efe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:02:21 +0000 Subject: [PATCH 101/125] Bump github/codeql-action from 3.27.4 to 3.27.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.4 to 3.27.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/ea9e4e37992a54ee68a9622e985e60c8e8f12d9f...f09c1c0a94de965c15400f5634aa42fac8fb8f88) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3047e7b..77773bc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -79,4 +79,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 4b242b4..30bbdc7 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 with: sarif_file: results.sarif From 45776c052f417971339f6dc7aa88ecca11d78ab8 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 14:58:05 +0100 Subject: [PATCH 102/125] Refactor client error handling and add concurrent send tests Updated tests to correctly assert the absence of an SMTP client on failure. Added concurrent sending tests for DialAndSendWithContext to improve test coverage and reliability. Also, refined the `AutoDiscover` and other client methods to ensure proper parameter use. --- client_test.go | 112 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/client_test.go b/client_test.go index f6d5161..36fb423 100644 --- a/client_test.go +++ b/client_test.go @@ -1772,11 +1772,8 @@ func TestClient_DialWithContext(t *testing.T) { t.Errorf("failed to close the client: %s", err) } }) - if client.smtpClient == nil { - t.Errorf("client with invalid HELO should still have a smtp client, got nil") - } - if !client.smtpClient.HasConnection() { - t.Errorf("client with invalid HELO should still have a smtp client connection, got nil") + if client.smtpClient != nil { + t.Error("client with invalid HELO should not have a smtp client") } }) t.Run("fail on base port and fallback", func(t *testing.T) { @@ -1825,11 +1822,8 @@ func TestClient_DialWithContext(t *testing.T) { if err = client.DialWithContext(ctxDial); err == nil { t.Fatalf("connection was supposed to fail, but didn't") } - if client.smtpClient == nil { - t.Fatalf("client has no smtp client") - } - if !client.smtpClient.HasConnection() { - t.Errorf("client has no connection") + if client.smtpClient != nil { + t.Fatalf("client is not supposed to have a smtp client") } }) t.Run("connect with failing auth", func(t *testing.T) { @@ -2297,6 +2291,7 @@ func TestClient_DialAndSendWithContext(t *testing.T) { t.Errorf("client was supposed to fail on dial") } }) + // https://github.com/wneessen/go-mail/issues/380 t.Run("concurrent sending via DialAndSendWithContext", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -2336,6 +2331,44 @@ func TestClient_DialAndSendWithContext(t *testing.T) { } wg.Wait() }) + // https://github.com/wneessen/go-mail/issues/385 + t.Run("concurrent sending via DialAndSendWithContext on receiver func", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + sender := testSender{client} + + ctxDial := context.Background() + wg := sync.WaitGroup{} + for i := 0; i < 5; i++ { + wg.Add(1) + msg := testMessage(t) + go func() { + defer wg.Done() + if goroutineErr := sender.Send(ctxDial, msg); goroutineErr != nil { + t.Errorf("failed to send message: %s", goroutineErr) + } + }() + } + wg.Wait() + }) } func TestClient_auth(t *testing.T) { @@ -2574,8 +2607,8 @@ func TestClient_authTypeAutoDiscover(t *testing.T) { } for _, tt := range tests { t.Run("AutoDiscover selects the strongest auth type: "+string(tt.expect), func(t *testing.T) { - client := &Client{smtpAuthType: SMTPAuthAutoDiscover, isEncrypted: tt.tls} - authType, err := client.authTypeAutoDiscover(tt.supported) + client := &Client{smtpAuthType: SMTPAuthAutoDiscover} + authType, err := client.authTypeAutoDiscover(tt.supported, tt.tls) if err != nil && !tt.shouldFail { t.Fatalf("failed to auto discover auth type: %s", err) } @@ -2748,7 +2781,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err != nil { + if err = client.sendSingleMsg(client.smtpClient, message); err != nil { t.Errorf("failed to send message: %s", err) } }) @@ -2791,7 +2824,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } }) @@ -2836,7 +2869,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -2886,7 +2919,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -2936,7 +2969,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -2986,7 +3019,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err != nil { + if err = client.sendSingleMsg(client.smtpClient, message); err != nil { t.Errorf("failed to send message: %s", err) } }) @@ -3029,7 +3062,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -3080,7 +3113,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -3131,7 +3164,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -3181,7 +3214,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -3231,7 +3264,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Errorf("client should have failed to send message") } var sendErr *SendError @@ -3281,7 +3314,7 @@ func TestClient_sendSingleMsg(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.sendSingleMsg(message); err == nil { + if err = client.sendSingleMsg(client.smtpClient, message); err == nil { t.Error("expected mail delivery to fail") } var sendErr *SendError @@ -3334,7 +3367,7 @@ func TestClient_checkConn(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.checkConn(); err != nil { + if err = client.checkConn(client.smtpClient); err != nil { t.Errorf("failed to check connection: %s", err) } }) @@ -3375,7 +3408,7 @@ func TestClient_checkConn(t *testing.T) { t.Errorf("failed to close client: %s", err) } }) - if err = client.checkConn(); err == nil { + if err = client.checkConn(client.smtpClient); err == nil { t.Errorf("client should have failed on connection check") } if !errors.Is(err, ErrNoActiveConnection) { @@ -3387,7 +3420,7 @@ func TestClient_checkConn(t *testing.T) { if err != nil { t.Fatalf("failed to create new client: %s", err) } - if err = client.checkConn(); err == nil { + if err = client.checkConn(client.smtpClient); err == nil { t.Errorf("client should have failed on connection check") } if !errors.Is(err, ErrNoActiveConnection) { @@ -3611,24 +3644,20 @@ func TestClient_XOAuth2OnFaker(t *testing.T) { } if err = c.DialWithContext(context.Background()); err == nil { t.Fatal("expected dial error got nil") - } else { - if !errors.Is(err, ErrXOauth2AuthNotSupported) { - t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) - } + } + if !errors.Is(err, ErrXOauth2AuthNotSupported) { + t.Fatalf("expected %v; got %v", ErrXOauth2AuthNotSupported, err) } if err = c.Close(); err != nil { t.Fatalf("disconnect from test server failed: %v", err) } client := strings.Split(wrote.String(), "\r\n") - if len(client) != 3 { - t.Fatalf("unexpected number of client requests got %d; want 3", len(client)) + if len(client) != 2 { + t.Fatalf("unexpected number of client requests got %d; want 2", len(client)) } if !strings.HasPrefix(client[0], "EHLO") { t.Fatalf("expected EHLO, got %q", client[0]) } - if client[1] != "QUIT" { - t.Fatalf("expected QUIT, got %q", client[3]) - } }) } @@ -3652,6 +3681,17 @@ func (f faker) SetDeadline(time.Time) error { return nil } func (f faker) SetReadDeadline(time.Time) error { return nil } func (f faker) SetWriteDeadline(time.Time) error { return nil } +type testSender struct { + client *Client +} + +func (t *testSender) Send(ctx context.Context, m *Msg) error { + if err := t.client.DialAndSendWithContext(ctx, m); err != nil { + return fmt.Errorf("failed to dial and send mail: %w", err) + } + return nil +} + // parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs. // In case of a decode error, it reports the error to the testing framework. func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { From f9e869061e9a7085d199be3408cebfaa708f9386 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 14:58:41 +0100 Subject: [PATCH 103/125] Add mutex locks to DSN option setters Mutex locks are added to ensure thread safety when setting DSN mail return and recipient notify options. This prevents data races in concurrent environments, improving the client's robustness. --- smtp/smtp.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/smtp/smtp.go b/smtp/smtp.go index 8a278ee..24a079f 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -601,12 +601,16 @@ func (c *Client) SetLogAuthData() { // SetDSNMailReturnOption sets the DSN mail return option for the Mail method func (c *Client) SetDSNMailReturnOption(d string) { + c.mutex.Lock() c.dsnmrtype = d + c.mutex.Unlock() } // SetDSNRcptNotifyOption sets the DSN recipient notify option for the Mail method func (c *Client) SetDSNRcptNotifyOption(d string) { + c.mutex.Lock() c.dsnrntype = d + c.mutex.Unlock() } // HasConnection checks if the client has an active connection. From 55884786be1d83687bba15ad105202103540c971 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 14:59:19 +0100 Subject: [PATCH 104/125] Refactor SMTP client functions to improve modularity Refactor `DialWithContext` to delegate client creation to `SMTPClientFromDialWithContext`. Add new methods `SendWithSMTPClient`, `CloseWithSMTPClient`, and `ResetWithSMTPClient` to handle specific actions on `smtp.Client`. Simplify `auth`, `sendSingleMsg`, and `tls` methods by passing client as parameters. --- client.go | 261 ++++++++++++++++++++++++++++++++------------------ client_120.go | 14 ++- 2 files changed, 180 insertions(+), 95 deletions(-) diff --git a/client.go b/client.go index a7dead8..de6479e 100644 --- a/client.go +++ b/client.go @@ -142,9 +142,6 @@ type ( // host is the hostname of the SMTP server we are connecting to. host string - // isEncrypted indicates wether the Client connection is encrypted or not. - isEncrypted bool - // logAuthData indicates whether authentication-related data should be logged. logAuthData bool @@ -931,62 +928,131 @@ func (c *Client) SetLogAuthData(logAuth bool) { // Returns: // - An error if the connection to the SMTP server fails or any subsequent command fails. func (c *Client) DialWithContext(dialCtx context.Context) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout)) - defer cancel() - - if c.dialContextFunc == nil { - netDialer := net.Dialer{} - c.dialContextFunc = netDialer.DialContext - - if c.useSSL { - tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig} - c.isEncrypted = true - c.dialContextFunc = tlsDialer.DialContext - } - } - connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr()) - if err != nil && c.fallbackPort != 0 { - // TODO: should we somehow log or append the previous error? - connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) - } + client, err := c.SMTPClientFromDialWithContext(dialCtx) if err != nil { return err } + c.mutex.Lock() + c.smtpClient = client + c.mutex.Unlock() + /* + ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout)) + defer cancel() + + isEncrypted := false + if c.dialContextFunc == nil { + netDialer := net.Dialer{} + c.dialContextFunc = netDialer.DialContext + + if c.useSSL { + tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig} + isEncrypted = true + c.dialContextFunc = tlsDialer.DialContext + } + } + connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr()) + if err != nil && c.fallbackPort != 0 { + // TODO: should we somehow log or append the previous error? + connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) + } + if err != nil { + return err + } + + client, err := smtp.NewClient(connection, c.host) + if err != nil { + return err + } + if client == nil { + return fmt.Errorf("SMTP client is nil") + } + c.smtpClient = client + + if c.logger != nil { + c.smtpClient.SetLogger(c.logger) + } + if c.useDebugLog { + c.smtpClient.SetDebugLog(true) + } + if c.logAuthData { + c.smtpClient.SetLogAuthData() + } + if err = c.smtpClient.Hello(c.helo); err != nil { + return err + } + + if err = c.tls(c.smtpClient, &isEncrypted); err != nil { + return err + } + + if err = c.auth(c.smtpClient, isEncrypted); err != nil { + return err + } + + */ + + return nil +} + +// SMTPClientFromDialWithContext is similar to DialWithContext but instead of storing the smtp.Client +// on the Client it will return the smtp.Client instead. +func (c *Client) SMTPClientFromDialWithContext(ctxDial context.Context) (*smtp.Client, error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + ctx, cancel := context.WithDeadline(ctxDial, time.Now().Add(c.connTimeout)) + defer cancel() + + isEncrypted := false + dialContextFunc := c.dialContextFunc + if c.dialContextFunc == nil { + netDialer := net.Dialer{} + dialContextFunc = netDialer.DialContext + if c.useSSL { + tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig} + isEncrypted = true + dialContextFunc = tlsDialer.DialContext + } + } + connection, err := dialContextFunc(ctx, "tcp", c.ServerAddr()) + if err != nil && c.fallbackPort != 0 { + // TODO: should we somehow log or append the previous error? + connection, err = dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) + } + if err != nil { + return nil, err + } client, err := smtp.NewClient(connection, c.host) if err != nil { - return err + return nil, err } if client == nil { - return fmt.Errorf("SMTP client is nil") + return nil, fmt.Errorf("SMTP client is nil") } - c.smtpClient = client if c.logger != nil { - c.smtpClient.SetLogger(c.logger) + client.SetLogger(c.logger) } if c.useDebugLog { - c.smtpClient.SetDebugLog(true) + client.SetDebugLog(true) } if c.logAuthData { - c.smtpClient.SetLogAuthData() + client.SetLogAuthData() } - if err = c.smtpClient.Hello(c.helo); err != nil { - return err + if err = client.Hello(c.helo); err != nil { + return nil, err } - if err = c.tls(); err != nil { - return err + if err = c.tls(client, &isEncrypted); err != nil { + return nil, err } - if err = c.auth(); err != nil { - return err + if err = c.auth(client, isEncrypted); err != nil { + return nil, err } - return nil + return client, nil } // Close terminates the connection to the SMTP server, returning an error if the disconnection @@ -999,10 +1065,14 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { // Returns: // - An error if the disconnection fails; otherwise, returns nil. func (c *Client) Close() error { - if c.smtpClient == nil || !c.smtpClient.HasConnection() { + return c.CloseWithSMTPClient(c.smtpClient) +} + +func (c *Client) CloseWithSMTPClient(client *smtp.Client) error { + if client == nil || !client.HasConnection() { return nil } - if err := c.smtpClient.Quit(); err != nil { + if err := client.Quit(); err != nil { return fmt.Errorf("failed to close SMTP client: %w", err) } @@ -1018,10 +1088,14 @@ func (c *Client) Close() error { // Returns: // - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil. func (c *Client) Reset() error { - if err := c.checkConn(); err != nil { + return c.ResetWithSMTPClient(c.smtpClient) +} + +func (c *Client) ResetWithSMTPClient(client *smtp.Client) error { + if err := c.checkConn(client); err != nil { return err } - if err := c.smtpClient.Reset(); err != nil { + if err := client.Reset(); err != nil { return fmt.Errorf("failed to send RSET to SMTP client: %w", err) } @@ -1061,19 +1135,20 @@ func (c *Client) DialAndSend(messages ...*Msg) error { // - An error if the connection fails, if sending the messages fails, or if closing the // connection fails; otherwise, returns nil. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { - c.sendMutex.Lock() - defer c.sendMutex.Unlock() - if err := c.DialWithContext(ctx); err != nil { + //c.sendMutex.Lock() + //defer c.sendMutex.Unlock() + client, err := c.SMTPClientFromDialWithContext(ctx) + if err != nil { return fmt.Errorf("dial failed: %w", err) } defer func() { - _ = c.Close() + _ = c.CloseWithSMTPClient(client) }() - if err := c.Send(messages...); err != nil { + if err := c.SendWithSMTPClient(client, messages...); err != nil { return fmt.Errorf("send failed: %w", err) } - if err := c.Close(); err != nil { + if err := c.CloseWithSMTPClient(client); err != nil { return fmt.Errorf("failed to close connection: %w", err) } return nil @@ -1098,16 +1173,17 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e // Returns: // - An error if the connection check fails, if no supported authentication method is found, // or if the authentication process fails. -func (c *Client) auth() error { +func (c *Client) auth(client *smtp.Client, isEnc bool) error { + var smtpAuth smtp.Auth if c.smtpAuth == nil && c.smtpAuthType != SMTPAuthNoAuth { - hasSMTPAuth, smtpAuthType := c.smtpClient.Extension("AUTH") + hasSMTPAuth, smtpAuthType := client.Extension("AUTH") if !hasSMTPAuth { return fmt.Errorf("server does not support SMTP AUTH") } authType := c.smtpAuthType if c.smtpAuthType == SMTPAuthAutoDiscover { - discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) + discoveredType, err := c.authTypeAutoDiscover(smtpAuthType, isEnc) if err != nil { return err } @@ -1119,74 +1195,74 @@ func (c *Client) auth() error { if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { return ErrPlainAuthNotSupported } - c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false) + smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false) case SMTPAuthPlainNoEnc: if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { return ErrPlainAuthNotSupported } - c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true) + smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true) case SMTPAuthLogin: if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) { return ErrLoginAuthNotSupported } - c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false) + smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false) case SMTPAuthLoginNoEnc: if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) { return ErrLoginAuthNotSupported } - c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true) + smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true) case SMTPAuthCramMD5: if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) { return ErrCramMD5AuthNotSupported } - c.smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass) + smtpAuth = smtp.CRAMMD5Auth(c.user, c.pass) case SMTPAuthXOAUTH2: if !strings.Contains(smtpAuthType, string(SMTPAuthXOAUTH2)) { return ErrXOauth2AuthNotSupported } - c.smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) + smtpAuth = smtp.XOAuth2Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1)) { return ErrSCRAMSHA1AuthNotSupported } - c.smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) + smtpAuth = smtp.ScramSHA1Auth(c.user, c.pass) case SMTPAuthSCRAMSHA256: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256)) { return ErrSCRAMSHA256AuthNotSupported } - c.smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) + smtpAuth = smtp.ScramSHA256Auth(c.user, c.pass) case SMTPAuthSCRAMSHA1PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA1PLUS)) { return ErrSCRAMSHA1PLUSAuthNotSupported } - tlsConnState, err := c.smtpClient.GetTLSConnectionState() + tlsConnState, err := client.GetTLSConnectionState() if err != nil { return err } - c.smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) + smtpAuth = smtp.ScramSHA1PlusAuth(c.user, c.pass, tlsConnState) case SMTPAuthSCRAMSHA256PLUS: if !strings.Contains(smtpAuthType, string(SMTPAuthSCRAMSHA256PLUS)) { return ErrSCRAMSHA256PLUSAuthNotSupported } - tlsConnState, err := c.smtpClient.GetTLSConnectionState() + tlsConnState, err := client.GetTLSConnectionState() if err != nil { return err } - c.smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState) + smtpAuth = smtp.ScramSHA256PlusAuth(c.user, c.pass, tlsConnState) default: return fmt.Errorf("unsupported SMTP AUTH type %q", c.smtpAuthType) } } - if c.smtpAuth != nil { - if err := c.smtpClient.Auth(c.smtpAuth); err != nil { + if smtpAuth != nil { + if err := client.Auth(smtpAuth); err != nil { return fmt.Errorf("SMTP AUTH failed: %w", err) } } return nil } -func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { +func (c *Client) authTypeAutoDiscover(supported string, isEnc bool) (SMTPAuthType, error) { if supported == "" { return "", ErrNoSupportedAuthDiscovered } @@ -1194,7 +1270,7 @@ func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin, } - if !c.isEncrypted { + if !isEnc { preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5} } mechs := strings.Split(supported, " ") @@ -1231,13 +1307,13 @@ func sliceContains(slice []string, item string) bool { // // Returns: // - An error if any part of the sending process fails; otherwise, returns nil. -func (c *Client) sendSingleMsg(message *Msg) error { - c.mutex.Lock() - defer c.mutex.Unlock() - escSupport, _ := c.smtpClient.Extension("ENHANCEDSTATUSCODES") +func (c *Client) sendSingleMsg(client *smtp.Client, message *Msg) error { + c.mutex.RLock() + defer c.mutex.RUnlock() + escSupport, _ := client.Extension("ENHANCEDSTATUSCODES") if message.encoding == NoEncoding { - if ok, _ := c.smtpClient.Extension("8BITMIME"); !ok { + if ok, _ := client.Extension("8BITMIME"); !ok { return &SendError{Reason: ErrNoUnencoded, isTemp: false, affectedMsg: message} } } @@ -1260,16 +1336,16 @@ func (c *Client) sendSingleMsg(message *Msg) error { if c.requestDSN { if c.dsnReturnType != "" { - c.smtpClient.SetDSNMailReturnOption(string(c.dsnReturnType)) + client.SetDSNMailReturnOption(string(c.dsnReturnType)) } } - if err = c.smtpClient.Mail(from); err != nil { + if err = client.Mail(from); err != nil { retError := &SendError{ Reason: ErrSMTPMailFrom, errlist: []error{err}, isTemp: isTempError(err), affectedMsg: message, errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport), } - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { + if resetSendErr := client.Reset(); resetSendErr != nil { retError.errlist = append(retError.errlist, resetSendErr) } return retError @@ -1279,9 +1355,9 @@ func (c *Client) sendSingleMsg(message *Msg) error { rcptSendErr.errlist = make([]error, 0) rcptSendErr.rcpt = make([]string, 0) rcptNotifyOpt := strings.Join(c.dsnRcptNotifyType, ",") - c.smtpClient.SetDSNRcptNotifyOption(rcptNotifyOpt) + client.SetDSNRcptNotifyOption(rcptNotifyOpt) for _, rcpt := range rcpts { - if err = c.smtpClient.Rcpt(rcpt); err != nil { + if err = client.Rcpt(rcpt); err != nil { rcptSendErr.Reason = ErrSMTPRcptTo rcptSendErr.errlist = append(rcptSendErr.errlist, err) rcptSendErr.rcpt = append(rcptSendErr.rcpt, rcpt) @@ -1292,12 +1368,12 @@ func (c *Client) sendSingleMsg(message *Msg) error { } } if hasError { - if resetSendErr := c.smtpClient.Reset(); resetSendErr != nil { + if resetSendErr := client.Reset(); resetSendErr != nil { rcptSendErr.errlist = append(rcptSendErr.errlist, resetSendErr) } return rcptSendErr } - writer, err := c.smtpClient.Data() + writer, err := client.Data() if err != nil { return &SendError{ Reason: ErrSMTPData, errlist: []error{err}, isTemp: isTempError(err), @@ -1322,7 +1398,7 @@ func (c *Client) sendSingleMsg(message *Msg) error { } message.isDelivered = true - if err = c.Reset(); err != nil { + if err = c.ResetWithSMTPClient(client); err != nil { return &SendError{ Reason: ErrSMTPReset, errlist: []error{err}, isTemp: isTempError(err), affectedMsg: message, errcode: errorCode(err), @@ -1344,21 +1420,24 @@ func (c *Client) sendSingleMsg(message *Msg) error { // Returns: // - An error if there is no active connection, if the NOOP command fails, or if extending // the deadline fails; otherwise, returns nil. -func (c *Client) checkConn() error { - if c.smtpClient == nil { +func (c *Client) checkConn(client *smtp.Client) error { + if client == nil { return ErrNoActiveConnection } - if !c.smtpClient.HasConnection() { + if !client.HasConnection() { return ErrNoActiveConnection } - if !c.noNoop { - if err := c.smtpClient.Noop(); err != nil { + c.mutex.RLock() + noNoop := c.noNoop + c.mutex.RUnlock() + if !noNoop { + if err := client.Noop(); err != nil { return ErrNoActiveConnection } } - if err := c.smtpClient.UpdateDeadline(c.connTimeout); err != nil { + if err := client.UpdateDeadline(c.connTimeout); err != nil { return ErrDeadlineExtendFailed } return nil @@ -1405,10 +1484,10 @@ func (c *Client) setDefaultHelo() error { // Returns: // - An error if there is no active connection, if STARTTLS is required but not supported, // or if there are issues during the TLS handshake; otherwise, returns nil. -func (c *Client) tls() error { +func (c *Client) tls(client *smtp.Client, isEnc *bool) error { if !c.useSSL && c.tlspolicy != NoTLS { hasStartTLS := false - extension, _ := c.smtpClient.Extension("STARTTLS") + extension, _ := client.Extension("STARTTLS") if c.tlspolicy == TLSMandatory { hasStartTLS = true if !extension { @@ -1422,21 +1501,21 @@ func (c *Client) tls() error { } } if hasStartTLS { - if err := c.smtpClient.StartTLS(c.tlsconfig); err != nil { + if err := client.StartTLS(c.tlsconfig); err != nil { return err } } - tlsConnState, err := c.smtpClient.GetTLSConnectionState() + tlsConnState, err := client.GetTLSConnectionState() if err != nil { switch { case errors.Is(err, smtp.ErrNonTLSConnection): - c.isEncrypted = false + *isEnc = false return nil default: return fmt.Errorf("failed to get TLS connection state: %w", err) } } - c.isEncrypted = tlsConnState.HandshakeComplete + *isEnc = tlsConnState.HandshakeComplete } return nil } diff --git a/client_120.go b/client_120.go index 67c5b5e..16e2d0b 100644 --- a/client_120.go +++ b/client_120.go @@ -9,6 +9,8 @@ package mail import ( "errors" + + "github.com/wneessen/go-mail/smtp" ) // Send attempts to send one or more Msg using the Client connection to the SMTP server. @@ -27,11 +29,15 @@ import ( // Returns: // - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) (returnErr error) { + return c.SendWithSMTPClient(c.smtpClient, messages...) +} + +func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) (returnErr error) { escSupport := false - if c.smtpClient != nil { - escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES") + if client != nil { + escSupport, _ = client.Extension("ENHANCEDSTATUSCODES") } - if err := c.checkConn(); err != nil { + if err := c.checkConn(client); err != nil { returnErr = &SendError{ Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport), @@ -45,7 +51,7 @@ func (c *Client) Send(messages ...*Msg) (returnErr error) { }() for id, message := range messages { - if sendErr := c.sendSingleMsg(message); sendErr != nil { + if sendErr := c.sendSingleMsg(client, message); sendErr != nil { messages[id].sendError = sendErr errs = append(errs, sendErr) } From be4201b05a885c48ff1f0c416549f1f6a680b413 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 15:29:22 +0100 Subject: [PATCH 105/125] Refactor debug logging and logger settings in Client Separated debug logging and logger setting methods to include SMTP client parameter for better encapsulation. Removed commented-out code for cleaner and more manageable codebase. --- client.go | 82 ++++++++++++------------------------------------------- 1 file changed, 18 insertions(+), 64 deletions(-) diff --git a/client.go b/client.go index de6479e..637e299 100644 --- a/client.go +++ b/client.go @@ -812,9 +812,15 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) { // Parameters: // - val: A boolean value indicating whether to enable (true) or disable (false) debug logging. func (c *Client) SetDebugLog(val bool) { + c.SetDebugLogWithSMTPClient(c.smtpClient, val) +} + +func (c *Client) SetDebugLogWithSMTPClient(client *smtp.Client, val bool) { + c.mutex.Lock() + defer c.mutex.Unlock() c.useDebugLog = val - if c.smtpClient != nil { - c.smtpClient.SetDebugLog(val) + if client != nil { + client.SetDebugLog(val) } } @@ -827,9 +833,15 @@ func (c *Client) SetDebugLog(val bool) { // Parameters: // - logger: A logger that satisfies the log.Logger interface to be set for the Client. func (c *Client) SetLogger(logger log.Logger) { + c.SetLoggerWithSMTPClient(c.smtpClient, logger) +} + +func (c *Client) SetLoggerWithSMTPClient(client *smtp.Client, logger log.Logger) { + c.mutex.Lock() + defer c.mutex.Unlock() c.logger = logger - if c.smtpClient != nil { - c.smtpClient.SetLogger(logger) + if client != nil { + client.SetLogger(logger) } } @@ -935,62 +947,6 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { c.mutex.Lock() c.smtpClient = client c.mutex.Unlock() - /* - ctx, cancel := context.WithDeadline(dialCtx, time.Now().Add(c.connTimeout)) - defer cancel() - - isEncrypted := false - if c.dialContextFunc == nil { - netDialer := net.Dialer{} - c.dialContextFunc = netDialer.DialContext - - if c.useSSL { - tlsDialer := tls.Dialer{NetDialer: &netDialer, Config: c.tlsconfig} - isEncrypted = true - c.dialContextFunc = tlsDialer.DialContext - } - } - connection, err := c.dialContextFunc(ctx, "tcp", c.ServerAddr()) - if err != nil && c.fallbackPort != 0 { - // TODO: should we somehow log or append the previous error? - connection, err = c.dialContextFunc(ctx, "tcp", c.serverFallbackAddr()) - } - if err != nil { - return err - } - - client, err := smtp.NewClient(connection, c.host) - if err != nil { - return err - } - if client == nil { - return fmt.Errorf("SMTP client is nil") - } - c.smtpClient = client - - if c.logger != nil { - c.smtpClient.SetLogger(c.logger) - } - if c.useDebugLog { - c.smtpClient.SetDebugLog(true) - } - if c.logAuthData { - c.smtpClient.SetLogAuthData() - } - if err = c.smtpClient.Hello(c.helo); err != nil { - return err - } - - if err = c.tls(c.smtpClient, &isEncrypted); err != nil { - return err - } - - if err = c.auth(c.smtpClient, isEncrypted); err != nil { - return err - } - - */ - return nil } @@ -1135,8 +1091,6 @@ func (c *Client) DialAndSend(messages ...*Msg) error { // - An error if the connection fails, if sending the messages fails, or if closing the // connection fails; otherwise, returns nil. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { - //c.sendMutex.Lock() - //defer c.sendMutex.Unlock() client, err := c.SMTPClientFromDialWithContext(ctx) if err != nil { return fmt.Errorf("dial failed: %w", err) @@ -1145,10 +1099,10 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e _ = c.CloseWithSMTPClient(client) }() - if err := c.SendWithSMTPClient(client, messages...); err != nil { + if err = c.SendWithSMTPClient(client, messages...); err != nil { return fmt.Errorf("send failed: %w", err) } - if err := c.CloseWithSMTPClient(client); err != nil { + if err = c.CloseWithSMTPClient(client); err != nil { return fmt.Errorf("failed to close connection: %w", err) } return nil From 3e504e6338495d0cdbfed401642b193909d5668a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 15:29:31 +0100 Subject: [PATCH 106/125] Lock sendMutex to ensure thread safety in Send Added a mutex lock and unlock around the Send function to prevent concurrent access issues and ensure thread safety. This change helps avoid race conditions when multiple goroutines attempt to send messages simultaneously. --- client_120.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client_120.go b/client_120.go index 16e2d0b..bc157a9 100644 --- a/client_120.go +++ b/client_120.go @@ -29,6 +29,8 @@ import ( // Returns: // - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) (returnErr error) { + c.sendMutex.Lock() + defer c.sendMutex.Unlock() return c.SendWithSMTPClient(c.smtpClient, messages...) } From 3553b657697cc9fef3180b91121523d0750494c0 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 15:38:04 +0100 Subject: [PATCH 107/125] Add new Send function to client.go and remove duplicates The new Send function in client.go adds thread safety by using a mutex. This change also removes duplicate Send functions from client_119.go and client_120.go, consolidating the logic in one place for easier maintenance. --- client.go | 6 ++++++ client_119.go | 17 +++++++++++------ client_120.go | 5 ----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/client.go b/client.go index 637e299..4ee68f0 100644 --- a/client.go +++ b/client.go @@ -1108,6 +1108,12 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e return nil } +func (c *Client) Send(messages ...*Msg) (returnErr error) { + c.sendMutex.Lock() + defer c.sendMutex.Unlock() + return c.SendWithSMTPClient(c.smtpClient, messages...) +} + // auth attempts to authenticate the client using SMTP AUTH mechanisms. It checks the connection, // determines the supported authentication methods, and applies the appropriate authentication // type. An error is returned if authentication fails. diff --git a/client_119.go b/client_119.go index a28f747..fb8c982 100644 --- a/client_119.go +++ b/client_119.go @@ -7,7 +7,11 @@ package mail -import "errors" +import ( + "errors" + + "github.com/wneessen/go-mail/smtp" +) // Send attempts to send one or more Msg using the Client connection to the SMTP server. // If the Client has no active connection to the server, Send will fail with an error. For each @@ -26,12 +30,13 @@ import "errors" // Returns: // - An error that represents the sending result, which may include multiple SendErrors if // any occurred; otherwise, returns nil. -func (c *Client) Send(messages ...*Msg) error { + +func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) error { escSupport := false - if c.smtpClient != nil { - escSupport, _ = c.smtpClient.Extension("ENHANCEDSTATUSCODES") + if client != nil { + escSupport, _ = client.Extension("ENHANCEDSTATUSCODES") } - if err := c.checkConn(); err != nil { + if err := c.checkConn(client); err != nil { return &SendError{ Reason: ErrConnCheck, errlist: []error{err}, isTemp: isTempError(err), errcode: errorCode(err), enhancedStatusCode: enhancedStatusCode(err, escSupport), @@ -39,7 +44,7 @@ func (c *Client) Send(messages ...*Msg) error { } var errs []*SendError for id, message := range messages { - if sendErr := c.sendSingleMsg(message); sendErr != nil { + if sendErr := c.sendSingleMsg(client, message); sendErr != nil { messages[id].sendError = sendErr var msgSendErr *SendError diff --git a/client_120.go b/client_120.go index bc157a9..e9ce8dc 100644 --- a/client_120.go +++ b/client_120.go @@ -28,11 +28,6 @@ import ( // // Returns: // - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. -func (c *Client) Send(messages ...*Msg) (returnErr error) { - c.sendMutex.Lock() - defer c.sendMutex.Unlock() - return c.SendWithSMTPClient(c.smtpClient, messages...) -} func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) (returnErr error) { escSupport := false From 4c107f4645526a2d2e76c88c323363308f48708b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 15:55:45 +0100 Subject: [PATCH 108/125] Refactor Client methods to handle smtp.Client parameter Updated several Client methods to accept a smtp.Client pointer, allowing for more flexible and explicit SMTP client management. Added detailed parameter descriptions and extended error handling documentation. --- client.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++-- client_119.go | 10 ++++---- client_120.go | 20 ++++++++------- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/client.go b/client.go index 4ee68f0..662d988 100644 --- a/client.go +++ b/client.go @@ -803,7 +803,7 @@ func (c *Client) SetSSLPort(ssl bool, fallback bool) { } // SetDebugLog sets or overrides whether the Client is using debug logging. The debug logger will log incoming -// and outgoing communication between the Client and the server to os.Stderr. +// and outgoing communication between the client and the server to log.Logger that is defined on the Client. // // Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using // SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use @@ -815,6 +815,17 @@ func (c *Client) SetDebugLog(val bool) { c.SetDebugLogWithSMTPClient(c.smtpClient, val) } +// SetDebugLogWithSMTPClient sets or overrides whether the provided smtp.Client is using debug logging. +// The debug logger will log incoming and outgoing communication between the client and the server to +// log.Logger that is defined on the Client. +// +// Note: The SMTP communication might include unencrypted authentication data, depending on whether you are using +// SMTP authentication and the type of authentication mechanism. This could pose a data protection risk. Use +// debug logging with caution. +// +// Parameters: +// - client: A pointer to the smtp.Client that handles the connection to the server. +// - val: A boolean value indicating whether to enable (true) or disable (false) debug logging. func (c *Client) SetDebugLogWithSMTPClient(client *smtp.Client, val bool) { c.mutex.Lock() defer c.mutex.Unlock() @@ -836,6 +847,15 @@ func (c *Client) SetLogger(logger log.Logger) { c.SetLoggerWithSMTPClient(c.smtpClient, logger) } +// SetLoggerWithSMTPClient sets or overrides the custom logger currently used by the provided smtp.Client. +// The logger must satisfy the log.Logger interface and is only utilized when debug logging is enabled on +// the provided smtp.Client. +// +// By default, log.Stdlog is used if no custom logger is provided. +// +// Parameters: +// - client: A pointer to the smtp.Client that handles the connection to the server. +// - logger: A logger that satisfies the log.Logger interface to be set for the Client. func (c *Client) SetLoggerWithSMTPClient(client *smtp.Client, logger log.Logger) { c.mutex.Lock() defer c.mutex.Unlock() @@ -1024,6 +1044,19 @@ func (c *Client) Close() error { return c.CloseWithSMTPClient(c.smtpClient) } +// CloseWithSMTPClient terminates the connection of the provided smtp.Client to the SMTP server, +// returning an error if the disconnection fails. If the connection is already closed, this +// method is a no-op and disregards any error. +// +// This function checks if the smtp.Client connection is active. If not, it simply returns +// without any action. If the connection is active, it attempts to gracefully close the +// connection using the Quit method. +// +// Parameters: +// - client: A pointer to the smtp.Client that handles the connection to the server. +// +// Returns: +// - An error if the disconnection fails; otherwise, returns nil. func (c *Client) CloseWithSMTPClient(client *smtp.Client) error { if client == nil || !client.HasConnection() { return nil @@ -1042,11 +1075,24 @@ func (c *Client) CloseWithSMTPClient(client *smtp.Client) error { // the command fails, an error is returned. // // Returns: -// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil. +// - An error if the connection check fails or if sending the RSET command fails; +// otherwise, returns nil. func (c *Client) Reset() error { return c.ResetWithSMTPClient(c.smtpClient) } +// ResetWithSMTPClient sends an SMTP RSET command to the provided smtp.Client, to reset +// the state of the current SMTP session. +// +// This method checks the connection to the SMTP server and, if the connection is valid, +// it sends an RSET command to reset the session state. If the connection is invalid or +// the command fails, an error is returned. +// +// Parameters: +// - client: A pointer to the smtp.Client that handles the connection to the server. +// +// Returns: +// - An error if the connection check fails or if sending the RSET command fails; otherwise, returns nil. func (c *Client) ResetWithSMTPClient(client *smtp.Client) error { if err := c.checkConn(client); err != nil { return err @@ -1108,6 +1154,24 @@ func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) e return nil } +// Send attempts to send one or more Msg using the SMTP client that is assigned to the Client. +// If the Client has no active connection to the server, Send will fail with an error. For +// each of the provided Msg, it will associate a SendError with the Msg in case of a +// transmission or delivery error. +// +// This method first checks for an active connection to the SMTP server. If the connection is +// not valid, it returns a SendError. It then iterates over the provided messages, attempting +// to send each one. If an error occurs during sending, the method records the error and +// associates it with the corresponding Msg. If multiple errors are encountered, it aggregates +// them into a single SendError to be returned. +// +// Parameters: +// - client: A pointer to the smtp.Client that holds the connection to the SMTP server +// - messages: A variadic list of pointers to Msg objects to be sent. +// +// Returns: +// - An error that represents the sending result, which may include multiple SendErrors if +// any occurred; otherwise, returns nil. func (c *Client) Send(messages ...*Msg) (returnErr error) { c.sendMutex.Lock() defer c.sendMutex.Unlock() diff --git a/client_119.go b/client_119.go index fb8c982..96d18a7 100644 --- a/client_119.go +++ b/client_119.go @@ -13,10 +13,10 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// Send attempts to send one or more Msg using the Client connection to the SMTP server. -// If the Client has no active connection to the server, Send will fail with an error. For each -// of the provided Msg, it will associate a SendError with the Msg in case of a transmission -// or delivery error. +// SendWithSMTPClient attempts to send one or more Msg using a provided smtp.Client with an +// established connection to the SMTP server. If the smtp.Client has no active connection to +// the server, SendWithSMTPClient will fail with an error. For each of the provided Msg, it +// will associate a SendError with the Msg in case of a transmission or delivery error. // // This method first checks for an active connection to the SMTP server. If the connection is // not valid, it returns a SendError. It then iterates over the provided messages, attempting @@ -25,12 +25,12 @@ import ( // them into a single SendError to be returned. // // Parameters: +// - client: A pointer to the smtp.Client that holds the connection to the SMTP server // - messages: A variadic list of pointers to Msg objects to be sent. // // Returns: // - An error that represents the sending result, which may include multiple SendErrors if // any occurred; otherwise, returns nil. - func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) error { escSupport := false if client != nil { diff --git a/client_120.go b/client_120.go index e9ce8dc..622e149 100644 --- a/client_120.go +++ b/client_120.go @@ -13,22 +13,24 @@ import ( "github.com/wneessen/go-mail/smtp" ) -// Send attempts to send one or more Msg using the Client connection to the SMTP server. -// If the Client has no active connection to the server, Send will fail with an error. For each -// of the provided Msg, it will associate a SendError with the Msg in case of a transmission -// or delivery error. +// SendWithSMTPClient attempts to send one or more Msg using a provided smtp.Client with an +// established connection to the SMTP server. If the smtp.Client has no active connection to +// the server, SendWithSMTPClient will fail with an error. For each of the provided Msg, it +// will associate a SendError with the Msg in case of a transmission or delivery error. // // This method first checks for an active connection to the SMTP server. If the connection is -// not valid, it returns an error wrapped in a SendError. It then iterates over the provided -// messages, attempting to send each one. If an error occurs during sending, the method records -// the error and associates it with the corresponding Msg. +// not valid, it returns a SendError. It then iterates over the provided messages, attempting +// to send each one. If an error occurs during sending, the method records the error and +// associates it with the corresponding Msg. If multiple errors are encountered, it aggregates +// them into a single SendError to be returned. // // Parameters: +// - client: A pointer to the smtp.Client that holds the connection to the SMTP server // - messages: A variadic list of pointers to Msg objects to be sent. // // Returns: -// - An error that aggregates any SendErrors encountered during the sending process; otherwise, returns nil. - +// - An error that represents the sending result, which may include multiple SendErrors if +// any occurred; otherwise, returns nil. func (c *Client) SendWithSMTPClient(client *smtp.Client, messages ...*Msg) (returnErr error) { escSupport := false if client != nil { From 7aba5212c47142eeafdfec1e6bf6e484e7a017c5 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 16:06:28 +0100 Subject: [PATCH 109/125] Rename method and improve context handling Renamed `SMTPClientFromDialWithContext` to `DialToSMTPClientWithContext` for clarity and consistency. Updated method parameters and documentation to standardize context usage across the codebase. --- client.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 662d988..98aea82 100644 --- a/client.go +++ b/client.go @@ -955,12 +955,12 @@ func (c *Client) SetLogAuthData(logAuth bool) { // SMTP server. // // Parameters: -// - dialCtx: The context.Context used to control the connection timeout and cancellation. +// - ctxDial: The context.Context used to control the connection timeout and cancellation. // // Returns: // - An error if the connection to the SMTP server fails or any subsequent command fails. -func (c *Client) DialWithContext(dialCtx context.Context) error { - client, err := c.SMTPClientFromDialWithContext(dialCtx) +func (c *Client) DialWithContext(ctxDial context.Context) error { + client, err := c.DialToSMTPClientWithContext(ctxDial) if err != nil { return err } @@ -970,9 +970,23 @@ func (c *Client) DialWithContext(dialCtx context.Context) error { return nil } -// SMTPClientFromDialWithContext is similar to DialWithContext but instead of storing the smtp.Client -// on the Client it will return the smtp.Client instead. -func (c *Client) SMTPClientFromDialWithContext(ctxDial context.Context) (*smtp.Client, error) { +// DialToSMTPClientWithContext establishes and configures a smtp.Client connection using +// the provided context. +// +// This function uses the provided context to manage the connection deadline and cancellation. +// It dials the SMTP server using the Client's configured DialContextFunc or a default dialer. +// If SSL is enabled, it uses a TLS connection. After successfully connecting, it initializes +// an smtp.Client, sends the HELO/EHLO command, and optionally performs STARTTLS and SMTP AUTH +// based on the Client's configuration. Debug and authentication logging are enabled if +// configured. +// +// Parameters: +// - ctxDial: The context used to control the connection timeout and cancellation. +// +// Returns: +// - A pointer to the initialized smtp.Client. +// - An error if the connection fails, the smtp.Client cannot be created, or any subsequent commands fail. +func (c *Client) DialToSMTPClientWithContext(ctxDial context.Context) (*smtp.Client, error) { c.mutex.RLock() defer c.mutex.RUnlock() @@ -1137,7 +1151,7 @@ func (c *Client) DialAndSend(messages ...*Msg) error { // - An error if the connection fails, if sending the messages fails, or if closing the // connection fails; otherwise, returns nil. func (c *Client) DialAndSendWithContext(ctx context.Context, messages ...*Msg) error { - client, err := c.SMTPClientFromDialWithContext(ctx) + client, err := c.DialToSMTPClientWithContext(ctx) if err != nil { return fmt.Errorf("dial failed: %w", err) } From b4d3b165edd1ab8069637d7916c6d915edac93aa Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 16:21:49 +0100 Subject: [PATCH 110/125] Add test for dialing SMTP client with context Introduce a new test, `TestClient_DialToSMTPClientWithContext`, to verify client behavior when establishing and closing an SMTP connection with context handling. Also added a sub-test to simulate and confirm failure scenarios during SMTP connection establishment. --- client_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/client_test.go b/client_test.go index 36fb423..10dd898 100644 --- a/client_test.go +++ b/client_test.go @@ -2742,6 +2742,83 @@ func TestClient_Send(t *testing.T) { }) } +func TestClient_DialToSMTPClientWithContext(t *testing.T) { + t.Run("establish a new client connection", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + PortAdder.Add(1) + serverPort := int(TestServerPortBase + PortAdder.Load()) + featureSet := "250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8" + go func() { + if err := simpleSMTPServer(ctx, t, &serverProps{ + FeatureSet: featureSet, + ListenPort: serverPort, + }); err != nil { + t.Errorf("failed to start test server: %s", err) + return + } + }() + time.Sleep(time.Millisecond * 30) + + ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS)) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + smtpClient, err := client.DialToSMTPClientWithContext(ctxDial) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + t.Skip("failed to connect to the test server due to timeout") + } + t.Fatalf("failed to connect to test server: %s", err) + } + t.Cleanup(func() { + if err := client.CloseWithSMTPClient(smtpClient); err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + t.Skip("failed to close the test server connection due to timeout") + } + t.Errorf("failed to close client: %s", err) + } + }) + if smtpClient == nil { + t.Fatal("expected SMTP client, got nil") + } + if !smtpClient.HasConnection() { + t.Fatal("expected connection on smtp client") + } + if ok, _ := smtpClient.Extension("DSN"); !ok { + t.Error("expected DSN extension but it was not found") + } + }) + t.Run("dial to SMTP server fails on first client writeFile", func(t *testing.T) { + var fake faker + fake.ReadWriter = struct { + io.Reader + io.Writer + }{ + failReadWriteSeekCloser{}, + failReadWriteSeekCloser{}, + } + + ctxDial, cancelDial := context.WithTimeout(context.Background(), time.Millisecond*500) + t.Cleanup(cancelDial) + + client, err := NewClient(DefaultHost, WithDialContextFunc(getFakeDialFunc(fake))) + if err != nil { + t.Fatalf("failed to create new client: %s", err) + } + _, err = client.DialToSMTPClientWithContext(ctxDial) + if err == nil { + t.Fatal("expected connection to fake to fail") + } + }) + +} + func TestClient_sendSingleMsg(t *testing.T) { t.Run("connect and send email", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) From c61aad4fcb8cfbbfa14c4b9225d5c93f7fb07464 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 16:22:38 +0100 Subject: [PATCH 111/125] Remove extra blank line in client_test.go This change removes an unnecessary blank line at the end of the TestClient_sendSingleMsg function in the client_test.go file. Keeping the code clean and properly formatted improves readability and maintainability. --- client_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/client_test.go b/client_test.go index 10dd898..195a797 100644 --- a/client_test.go +++ b/client_test.go @@ -2816,7 +2816,6 @@ func TestClient_DialToSMTPClientWithContext(t *testing.T) { t.Fatal("expected connection to fake to fail") } }) - } func TestClient_sendSingleMsg(t *testing.T) { From 6ebc60d1df74e2fb764f6dec0221ee5152cb4f8a Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 22 Nov 2024 16:22:53 +0100 Subject: [PATCH 112/125] Remove redundant nil check for SMTP client The removed check for a nil SMTP client was redundant because the previous error handling already covers this case. Streamlining this part of the code improves readability and reduces unnecessary checks. --- client.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/client.go b/client.go index 98aea82..028e277 100644 --- a/client.go +++ b/client.go @@ -1017,9 +1017,6 @@ func (c *Client) DialToSMTPClientWithContext(ctxDial context.Context) (*smtp.Cli if err != nil { return nil, err } - if client == nil { - return nil, fmt.Errorf("SMTP client is nil") - } if c.logger != nil { client.SetLogger(c.logger) From 2f08829fa3c19a93d774cf99dbf55bd3a3ed6376 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sun, 24 Nov 2024 12:25:37 +0100 Subject: [PATCH 113/125] Remove unused opts parameter docstring The opts parameter documentation was removed as it is no longer used in the quicksend function. This change helps in maintaining accurate and up-to-date code documentation and reduces potential confusion. --- quicksend.go | 1 - 1 file changed, 1 deletion(-) diff --git a/quicksend.go b/quicksend.go index 797b4bd..204971f 100644 --- a/quicksend.go +++ b/quicksend.go @@ -41,7 +41,6 @@ var testHookTLSConfig func() *tls.Config // nil, except for tests // - rcpts: A slice of strings of receipient addresses. // - subject: The subject line as string. // - content: A byte slice of the mail content -// - opts: Optional parameters for customizing the body part. // // Returns: // - A pointer to the generated Msg. From 8f8b5079c3036401bfeed3bf4ac2b6bc559a83eb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 26 Nov 2024 11:37:26 +0100 Subject: [PATCH 114/125] Improve filename sanitization in MIME headers Sanitize filenames to replace invalid characters before encoding them. This prevents control and special characters from causing issues in MIME headers and file systems. The `sanitizeFilename` function ensures these characters are replaced with underscores. --- msgwriter.go | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/msgwriter.go b/msgwriter.go index ed2b41e..18fa187 100644 --- a/msgwriter.go +++ b/msgwriter.go @@ -273,7 +273,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { mimeType = string(file.ContentType) } file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType, - mw.encoder.Encode(mw.charset.String(), file.Name))) + mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name)))) } if _, ok := file.getHeader(HeaderContentTransferEnc); !ok { @@ -285,7 +285,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { if file.Desc != "" { if _, ok := file.getHeader(HeaderContentDescription); !ok { - file.setHeader(HeaderContentDescription, file.Desc) + file.setHeader(HeaderContentDescription, mw.encoder.Encode(mw.charset.String(), file.Desc)) } } @@ -295,12 +295,12 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) { disposition = "attachment" } file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, - disposition, mw.encoder.Encode(mw.charset.String(), file.Name))) + disposition, mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name)))) } if !isAttachment { if _, ok := file.getHeader(HeaderContentID); !ok { - file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", file.Name)) + file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", sanitizeFilename(file.Name))) } } if mw.depth == 0 { @@ -498,3 +498,33 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin mw.bytesWritten += n } } + +// sanitizeFilename sanitizes a given filename string by replacing specific unwanted characters with +// an underscore ('_'). +// +// This method replaces any control character and any special character that is problematic for +// MIME headers and file systems with an underscore ('_') character. +// +// The following characters are replaced +// - Any control character (US-ASCII < 32) +// - ", /, :, <, >, ?, \, |, [DEL] +// +// Parameters: +// - input: A string of a filename that is supposed to be sanitized +// +// Returns: +// - A string representing the sanitized version of the filename +func sanitizeFilename(input string) string { + var sanitized strings.Builder + for i := 0; i < len(input); i++ { + // We do not allow control characters in file names. + if input[i] < 32 || input[i] == 34 || input[i] == 47 || input[i] == 58 || + input[i] == 60 || input[i] == 62 || input[i] == 63 || input[i] == 92 || + input[i] == 124 || input[i] == 127 { + sanitized.WriteRune('_') + continue + } + sanitized.WriteByte(input[i]) + } + return sanitized.String() +} From b051471f8d5cc8e5b13c7259269588c010d47115 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 26 Nov 2024 12:02:15 +0100 Subject: [PATCH 115/125] Add tests for the sanitizeFilename function This commit introduces a series of tests for the sanitizeFilename function in msgwriter_test.go. The tests cover various edge cases to ensure filenames are sanitized correctly by replacing or removing invalid characters. These additions will help maintain the integrity and reliability of filename sanitization in the codebase. --- msgwriter_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/msgwriter_test.go b/msgwriter_test.go index 1b2a254..a3d2e56 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -676,3 +676,33 @@ func TestMsgWriter_writeBody(t *testing.T) { } }) } + +func TestMsgWriter_sanitizeFilename(t *testing.T) { + tests := []struct { + given string + want string + }{ + {"test.txt", "test.txt"}, + {"test file.txt", "test file.txt"}, + {"test\\ file.txt", "test_ file.txt"}, + {`"test" file.txt`, "_test_ file.txt"}, + {`test file .txt`, "test_file_.txt"}, + {"test\r\nfile.txt", "test__file.txt"}, + {"test\x22file.txt", "test_file.txt"}, + {"test\x2ffile.txt", "test_file.txt"}, + {"test\x3afile.txt", "test_file.txt"}, + {"test\x3cfile.txt", "test_file.txt"}, + {"test\x3efile.txt", "test_file.txt"}, + {"test\x3ffile.txt", "test_file.txt"}, + {"test\x5cfile.txt", "test_file.txt"}, + {"test\x7cfile.txt", "test_file.txt"}, + {"test\x7ffile.txt", "test_file.txt"}, + } + for _, tt := range tests { + t.Run(tt.given+"=>"+tt.want, func(t *testing.T) { + if got := sanitizeFilename(tt.given); got != tt.want { + t.Errorf("sanitizeFilename failed, expected: %q, got: %q", tt.want, got) + } + }) + } +} From fe765cd49e6a55fb1637488e50b8407769ed566e Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 26 Nov 2024 16:17:34 +0100 Subject: [PATCH 116/125] Add filename sanitization tests for attachments Introduced tests to validate filename sanitization for attachments, ensuring disallowed characters are handled correctly. These tests cover various scenarios, including different character sets such as Japanese, Chinese, and Cyrillic. --- msgwriter_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/msgwriter_test.go b/msgwriter_test.go index a3d2e56..31aae21 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "mime" + "os" "runtime" "strings" "testing" @@ -304,6 +305,81 @@ func TestMsgWriter_addFiles(t *testing.T) { charset: CharsetUTF8, encoder: getEncoder(EncodingQP), } + tests := []struct { + name string + filename string + expect string + }{ + {"normal US-ASCII filename", "test.txt", "test.txt"}, + {"normal US-ASCII filename with space", "test file.txt", "test file.txt"}, + {"filename with new lines", "test\r\n.txt", "test__.txt"}, + {"filename with disallowed character:\x22", "test\x22.txt", "test_.txt"}, + {"filename with disallowed character:\x2f", "test\x2f.txt", "test_.txt"}, + {"filename with disallowed character:\x3a", "test\x3a.txt", "test_.txt"}, + {"filename with disallowed character:\x3c", "test\x3c.txt", "test_.txt"}, + {"filename with disallowed character:\x3e", "test\x3e.txt", "test_.txt"}, + {"filename with disallowed character:\x3f", "test\x3f.txt", "test_.txt"}, + {"filename with disallowed character:\x5c", "test\x5c.txt", "test_.txt"}, + {"filename with disallowed character:\x7c", "test\x7c.txt", "test_.txt"}, + {"filename with disallowed character:\x7f", "test\x7f.txt", "test_.txt"}, + { + "japanese characters filename", "添付ファイル.txt", + "=?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB.txt?=", + }, + { + "simplified chinese characters filename", "测试附件文件.txt", + "=?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=99=84=E4=BB=B6=E6=96=87=E4=BB=B6.txt?=", + }, + { + "cyrillic characters filename", "Тестовый прикрепленный файл.txt", + "=?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9_=D0=BF=D1=80?= " + + "=?UTF-8?q?=D0=B8=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B?= " + + "=?UTF-8?q?=D0=B9_=D1=84=D0=B0=D0=B9=D0=BB.txt?=", + }, + } + for _, tt := range tests { + t.Run("addFile with filename sanitization: "+tt.name, func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t) + tmpfile, err := os.CreateTemp("", "attachment.*.tmp") + if err != nil { + t.Fatalf("failed to create tempfile: %s", err) + } + t.Cleanup(func() { + if err = os.Remove(tmpfile.Name()); err != nil { + t.Errorf("failed to remove tempfile: %s", err) + } + }) + + source, err := os.Open("testdata/attachment.txt") + if err != nil { + t.Fatalf("failed to open source file: %s", err) + } + if _, err = io.Copy(tmpfile, source); err != nil { + t.Fatalf("failed to copy source file: %s", err) + } + if err = tmpfile.Close(); err != nil { + t.Fatalf("failed to close tempfile: %s", err) + } + if err = source.Close(); err != nil { + t.Fatalf("failed to close source file: %s", err) + } + message.AttachFile(tmpfile.Name(), WithFileName(tt.filename)) + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + ctExpect := fmt.Sprintf(`Content-Type: text/plain; charset=utf-8; name="%s"`, tt.expect) + cdExpect := fmt.Sprintf(`Content-Disposition: attachment; filename="%s"`, tt.expect) + if !strings.Contains(buffer.String(), ctExpect) { + t.Errorf("expected content-type: %q, got: %q", ctExpect, buffer.String()) + } + if !strings.Contains(buffer.String(), cdExpect) { + t.Errorf("expected content-disposition: %q, got: %q", cdExpect, buffer.String()) + } + }) + } t.Run("message with a single file attached", func(t *testing.T) { buffer := bytes.NewBuffer(nil) msgwriter.writer = buffer From 75c0e3319be3ab43833c9864d993297f74984837 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 26 Nov 2024 16:26:13 +0100 Subject: [PATCH 117/125] Simplify attachment test setup Removed temporary file creation and copying in msgwriter_test.go. Directly attach the source file during the test to streamline the setup process. This change reduces complexity and potential points of failure in the test code. --- msgwriter_test.go | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index 31aae21..d72c40c 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "mime" - "os" "runtime" "strings" "testing" @@ -342,30 +341,7 @@ func TestMsgWriter_addFiles(t *testing.T) { buffer := bytes.NewBuffer(nil) msgwriter.writer = buffer message := testMessage(t) - tmpfile, err := os.CreateTemp("", "attachment.*.tmp") - if err != nil { - t.Fatalf("failed to create tempfile: %s", err) - } - t.Cleanup(func() { - if err = os.Remove(tmpfile.Name()); err != nil { - t.Errorf("failed to remove tempfile: %s", err) - } - }) - - source, err := os.Open("testdata/attachment.txt") - if err != nil { - t.Fatalf("failed to open source file: %s", err) - } - if _, err = io.Copy(tmpfile, source); err != nil { - t.Fatalf("failed to copy source file: %s", err) - } - if err = tmpfile.Close(); err != nil { - t.Fatalf("failed to close tempfile: %s", err) - } - if err = source.Close(); err != nil { - t.Fatalf("failed to close source file: %s", err) - } - message.AttachFile(tmpfile.Name(), WithFileName(tt.filename)) + message.AttachFile("testdata/attachment.txt", WithFileName(tt.filename)) msgwriter.writeMsg(message) if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) From f61da9cc2ee066df9ab2e1d8ac2f91039710b525 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 26 Nov 2024 16:34:09 +0100 Subject: [PATCH 118/125] Update content-type handling for FreeBSD in msgwriter test Adjusted the expected content-type header in msgwriter tests to account for FreeBSD by setting it to 'application/octet-stream' instead of 'text/plain'. This ensures compatibility and correct behavior across different operating systems. --- msgwriter_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index d72c40c..3fd158c 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -346,8 +346,15 @@ func TestMsgWriter_addFiles(t *testing.T) { if msgwriter.err != nil { t.Errorf("msgWriter failed to write: %s", msgwriter.err) } - ctExpect := fmt.Sprintf(`Content-Type: text/plain; charset=utf-8; name="%s"`, tt.expect) + + var ctExpect string cdExpect := fmt.Sprintf(`Content-Disposition: attachment; filename="%s"`, tt.expect) + switch runtime.GOOS { + case "freebsd": + ctExpect = fmt.Sprintf(`Content-Type: application/octet-stream; charset=utf-8; name="%s"`, tt.expect) + default: + ctExpect = fmt.Sprintf(`Content-Type: text/plain; charset=utf-8; name="%s"`, tt.expect) + } if !strings.Contains(buffer.String(), ctExpect) { t.Errorf("expected content-type: %q, got: %q", ctExpect, buffer.String()) } From 06bee90a1ea068fe7d5a56463c74d06bf9715847 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Tue, 26 Nov 2024 16:38:30 +0100 Subject: [PATCH 119/125] Remove charset from octet-stream Content-Type on FreeBSD The charset parameter in the Content-Type header for octet-stream files on FreeBSD was removed. This aligns the behavior with the expected MIME type format for such files. Other platforms remain unchanged. --- msgwriter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgwriter_test.go b/msgwriter_test.go index 3fd158c..2c11406 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -351,7 +351,7 @@ func TestMsgWriter_addFiles(t *testing.T) { cdExpect := fmt.Sprintf(`Content-Disposition: attachment; filename="%s"`, tt.expect) switch runtime.GOOS { case "freebsd": - ctExpect = fmt.Sprintf(`Content-Type: application/octet-stream; charset=utf-8; name="%s"`, tt.expect) + ctExpect = fmt.Sprintf(`Content-Type: application/octet-stream; name="%s"`, tt.expect) default: ctExpect = fmt.Sprintf(`Content-Type: text/plain; charset=utf-8; name="%s"`, tt.expect) } From eb4f53a9fb10bb8404510bf76397793f83958cdc Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Sat, 30 Nov 2024 19:57:35 +0000 Subject: [PATCH 120/125] [StepSecurity] ci: Harden GitHub Actions Signed-off-by: StepSecurity Bot --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd31ef4..72c82a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,7 +161,7 @@ jobs: - name: Checkout Code uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # master - name: Run go test on FreeBSD - uses: vmactions/freebsd-vm@v1 + uses: vmactions/freebsd-vm@debf37ca7b7fa40e19c542ef7ba30d6054a706a4 # v1.1.5 with: usesh: true copyback: false From fb63a50a9c7dbbbd9aded2cc00114a663ecfa846 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:31:16 +0000 Subject: [PATCH 121/125] Bump sonarsource/sonarqube-scan-action from 4.0.0 to 4.1.0 Bumps [sonarsource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/94d4f8ac4aaefccd7fb84bff00b0aeb2d65fcd49...1b442ee39ac3fa7c2acdd410208dcb2bcfaae6c4) --- updated-dependencies: - dependency-name: sonarsource/sonarqube-scan-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c82a6..c48753b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: run: | go test -shuffle=on -race --coverprofile=./cov.out ./... - name: SonarQube scan - uses: sonarsource/sonarqube-scan-action@94d4f8ac4aaefccd7fb84bff00b0aeb2d65fcd49 # master + uses: sonarsource/sonarqube-scan-action@1b442ee39ac3fa7c2acdd410208dcb2bcfaae6c4 # master if: success() env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 2faeadff6d15806664a7d5a2268bc5837050a44d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:55:59 +0000 Subject: [PATCH 122/125] Bump github/codeql-action from 3.27.5 to 3.27.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.5 to 3.27.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f09c1c0a94de965c15400f5634aa42fac8fb8f88...aa578102511db1f4524ed59b8cc2bae4f6e88195) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 77773bc..24194c7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/autobuild@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -79,4 +79,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 30bbdc7..3fe1677 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6 with: sarif_file: results.sarif From bc46e064c850d65572c9f35c8b55f52cdda76ee1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:19:33 +0000 Subject: [PATCH 123/125] Bump golang.org/x/text from 0.20.0 to 0.21.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.20.0 to 0.21.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 72ce847..a0c9b50 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,5 @@ go 1.16 require ( golang.org/x/crypto v0.29.0 - golang.org/x/text v0.20.0 + golang.org/x/text v0.21.0 ) diff --git a/go.sum b/go.sum index 30d3dc0..7322e0e 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -55,8 +56,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 37b396be440dc417c2e61694121db47e728d1c4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:59:26 +0000 Subject: [PATCH 124/125] Bump golang.org/x/crypto from 0.29.0 to 0.30.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.30.0. - [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index a0c9b50..b98e082 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,6 @@ module github.com/wneessen/go-mail go 1.16 require ( - golang.org/x/crypto v0.29.0 + golang.org/x/crypto v0.30.0 golang.org/x/text v0.21.0 ) diff --git a/go.sum b/go.sum index 7322e0e..31c8dbd 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -26,7 +26,6 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -38,7 +37,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -47,7 +46,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -56,7 +55,6 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From a66e63d9744f3671aef38df642b2b6581f650ee8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:35:21 +0000 Subject: [PATCH 125/125] Bump codecov/codecov-action from 5.0.7 to 5.1.1 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.7 to 5.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/015f24e6818733317a2da2edd6290ab26238649a...7f8b4b4bde536c465e797be725718b88c5d95e0e) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c48753b..3c8911b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: go test -race -shuffle=on --coverprofile=coverage.coverprofile --covermode=atomic ./... - name: Upload coverage to Codecov if: success() - uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a # v5.0.7 + uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos lint: