mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 22:00:49 +01:00
Compare commits
No commits in common. "9410381bfc5bd749529085f8c534b9f4bde549d2" and "f37ab2457b632cd1e15445bc7b86e1c9e22fc2a4" have entirely different histories.
9410381bfc
...
f37ab2457b
7 changed files with 9 additions and 519 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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
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@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|
|
@ -72,10 +72,3 @@ 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,8 +3665,6 @@ 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
|
||||||
|
@ -3756,13 +3754,6 @@ 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() {
|
||||||
|
@ -3779,13 +3770,6 @@ 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)
|
||||||
|
@ -3846,13 +3830,6 @@ 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
112
quicksend.go
|
@ -1,112 +0,0 @@
|
||||||
// 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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,368 +0,0 @@
|
||||||
// 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