mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 22:00:49 +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 }}
|
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -73,7 +73,7 @@ jobs:
|
||||||
go: ['1.23']
|
go: ['1.23']
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
|
@ -95,7 +95,7 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -113,7 +113,7 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Run govulncheck
|
- name: Run govulncheck
|
||||||
|
@ -133,7 +133,7 @@ jobs:
|
||||||
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
|
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -178,7 +178,7 @@ jobs:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
@ -204,7 +204,7 @@ jobs:
|
||||||
TEST_PASS: ${{ secrets.TEST_PASS }}
|
TEST_PASS: ${{ secrets.TEST_PASS }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
|
@ -72,3 +72,10 @@ text = "G505:"
|
||||||
linters = ["gosec"]
|
linters = ["gosec"]
|
||||||
path = "smtp/smtp_test.go"
|
path = "smtp/smtp_test.go"
|
||||||
text = "G505:"
|
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.
|
// serverProps represents the configuration properties for the SMTP server.
|
||||||
type serverProps struct {
|
type serverProps struct {
|
||||||
|
BufferMutex sync.RWMutex
|
||||||
|
EchoBuffer io.Writer
|
||||||
FailOnAuth bool
|
FailOnAuth bool
|
||||||
FailOnDataInit bool
|
FailOnDataInit bool
|
||||||
FailOnDataClose bool
|
FailOnDataClose bool
|
||||||
|
@ -3754,6 +3756,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("failed to write line: %s", err)
|
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()
|
_ = writer.Flush()
|
||||||
}
|
}
|
||||||
writeOK := func() {
|
writeOK := func() {
|
||||||
|
@ -3770,6 +3779,13 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond)
|
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
|
var datastring string
|
||||||
data = strings.TrimSpace(data)
|
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)
|
t.Logf("failed to read data from connection: %s", derr)
|
||||||
break
|
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)
|
ddata = strings.TrimSpace(ddata)
|
||||||
if ddata == "." {
|
if ddata == "." {
|
||||||
if props.FailOnDataClose {
|
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