mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 05:40:50 +01:00
Compare commits
6 commits
f37ab2457b
...
9410381bfc
Author | SHA1 | Date | |
---|---|---|---|
9410381bfc | |||
f05654d5e5 | |||
7ee4e47c8e | |||
d30a4a73c6 | |||
25a0fb23a9 | |||
|
93fc646338 |
7 changed files with 519 additions and 9 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -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
|
||||
|
||||
|
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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:"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
112
quicksend.go
Normal file
112
quicksend.go
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
368
quicksend_test.go
Normal file
368
quicksend_test.go
Normal file
|
@ -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:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8"},
|
||||
{23, "RCPT TO:<valid-to@domain.tld>"},
|
||||
{30, "Subject: " + subject},
|
||||
{33, "From: <valid-from@domain.tld>"},
|
||||
{34, "To: <valid-to@domain.tld>"},
|
||||
{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:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8"},
|
||||
{23, "RCPT TO:<valid-to@domain.tld>"},
|
||||
{25, "RCPT TO:<valid-to@domain.tld>"},
|
||||
{27, "RCPT TO:<valid-to@domain.tld>"},
|
||||
{34, "Subject: " + subject},
|
||||
{37, "From: <valid-from@domain.tld>"},
|
||||
{38, "To: <valid-to@domain.tld>, <valid-to@domain.tld>, <valid-to@domain.tld>"},
|
||||
{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)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue