diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5b1ef9bf..39a41eb6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -153,7 +153,7 @@ jobs:
cancel-in-progress: true
strategy:
matrix:
- osver: ['14.1', '14.0', 13.4']
+ osver: ['14.2', '14.1', 13.4']
env:
TEST_BASEPORT: ${{ vars.TEST_BASEPORT }}
TEST_BASEPORT_SMTP: ${{ vars.TEST_BASEPORT_SMTP }}
diff --git a/msg_test.go b/msg_test.go
index bbe8703d..d90981f0 100644
--- a/msg_test.go
+++ b/msg_test.go
@@ -19,12 +19,21 @@ import (
"net"
"os"
"reflect"
+ "runtime"
"strings"
"testing"
ttpl "text/template"
"time"
)
+type msgContentTest struct {
+ line int
+ data string
+ exact bool
+ dataIsPrefix bool
+ dataIsSuffix bool
+}
+
var (
charsetTests = []struct {
name string
@@ -5939,6 +5948,260 @@ func TestMsg_WriteTo(t *testing.T) {
t.Errorf("expected S/MIME signing error to contain: %q, got: %s", expErr, err)
}
})
+ t.Run("WriteTo Multipart plain text with attachment", func(t *testing.T) {
+ message := testMessage(t)
+ message.AttachFile("testdata/attachment.txt")
+ buffer := bytes.NewBuffer(nil)
+ if _, err := message.WriteTo(buffer); err != nil {
+ t.Fatalf("failed to write message to buffer: %s", err)
+ }
+ fileContentType := "text/plain; charset=utf-8"
+ if runtime.GOOS == "freebsd" {
+ fileContentType = "application/octet-stream"
+ }
+ wants := []msgContentTest{
+ {0, "Date:", false, true, false},
+ {1, "MIME-Version: 1.0", true, true, false},
+ {2, "Message-ID: <", false, true, false},
+ {2, ">", false, false, true},
+ {8, "Content-Type: multipart/mixed;", true, true, false},
+ {9, " boundary=", false, true, false},
+ {10, "", true, false, false},
+ {11, "--", false, true, false},
+ {12, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {13, "Content-Type: text/plain; charset=UTF-8", true, true, false},
+ {14, "", true, false, false},
+ {15, "Testmail", true, true, false},
+ {16, "--", false, true, false},
+ {17, `Content-Disposition: attachment; filename="attachment.txt"`, true, true, false},
+ {18, `Content-Transfer-Encoding: base64`, true, true, false},
+ {19, `Content-Type: ` + fileContentType + `; name="attachment.txt"`, true, true, false},
+ {20, "", true, false, false},
+ {21, "VGhpcyBpcyBhIHRlc3Qg", false, true, false},
+ {22, "", true, false, false},
+ {23, "--", false, true, true},
+ }
+ checkMessageContent(t, buffer, wants)
+ })
+ t.Run("WriteTo Multipart plain text with alternative", func(t *testing.T) {
+ message := testMessage(t)
+ message.AddAlternativeString(TypeTextHTML, "
HTML alternative
")
+ buffer := bytes.NewBuffer(nil)
+ if _, err := message.WriteTo(buffer); err != nil {
+ t.Fatalf("failed to write message to buffer: %s", err)
+ }
+ wants := []msgContentTest{
+ {0, "Date:", false, true, false},
+ {1, "MIME-Version: 1.0", true, true, false},
+ {2, "Message-ID: <", false, true, false},
+ {2, ">", false, false, true},
+ {8, "Content-Type: multipart/alternative;", true, true, false},
+ {9, " boundary=", false, true, false},
+ {10, "", true, false, false},
+ {11, "--", false, true, false},
+ {12, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {13, "Content-Type: text/plain; charset=UTF-8", true, true, false},
+ {14, "", true, false, false},
+ {15, "Testmail", true, true, false},
+ {16, "--", false, true, false},
+ {17, `Content-Transfer-Encoding: quoted-printable`, true, true, false},
+ {18, `Content-Type: text/html; charset=UTF-8`, true, true, false},
+ {19, "", true, false, false},
+ {20, `HTML alternative
`, true, true, false},
+ {21, "--", false, true, true},
+ }
+ checkMessageContent(t, buffer, wants)
+ })
+ t.Run("WriteTo Multipart two alternative parts", func(t *testing.T) {
+ message := NewMsg()
+ if message == nil {
+ t.Fatal("failed to create new message")
+ }
+ if err := message.From(TestSenderValid); err != nil {
+ t.Errorf("failed to set sender address: %s", err)
+ }
+ if err := message.To(TestRcptValid); err != nil {
+ t.Errorf("failed to set recipient address: %s", err)
+ }
+ message.Subject("Testmail")
+ message.AddAlternativeString(TypeTextPlain, "Plain alternative")
+ message.AddAlternativeString(TypeTextHTML, "HTML main part
")
+ buffer := bytes.NewBuffer(nil)
+ if _, err := message.WriteTo(buffer); err != nil {
+ t.Fatalf("failed to write message to buffer: %s", err)
+ }
+ wants := []msgContentTest{
+ {0, "Date:", false, true, false},
+ {1, "MIME-Version: 1.0", true, true, false},
+ {2, "Message-ID: <", false, true, false},
+ {2, ">", false, false, true},
+ {8, "Content-Type: multipart/alternative;", true, true, false},
+ {9, " boundary=", false, true, false},
+ {10, "", true, false, false},
+ {11, "--", false, true, false},
+ {12, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {13, "Content-Type: text/plain; charset=UTF-8", true, true, false},
+ {14, "", true, false, false},
+ {15, "Plain alternative", true, true, false},
+ {16, "--", false, true, false},
+ {17, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {18, "Content-Type: text/html; charset=UTF-8", true, true, false},
+ {19, "", true, false, false},
+ {20, "HTML main part
", true, true, false},
+ {21, "--", false, true, true},
+ }
+ checkMessageContent(t, buffer, wants)
+ })
+ t.Run("WriteTo Multipart plain body, alternative html, attachment and embed", func(t *testing.T) {
+ message := testMessage(t)
+ message.AddAlternativeString(TypeTextHTML, "HTML alternative part
")
+ message.AttachFile("testdata/attachment.txt")
+ message.EmbedFile("testdata/embed.txt")
+ buffer := bytes.NewBuffer(nil)
+ if _, err := message.WriteTo(buffer); err != nil {
+ t.Fatalf("failed to write message to buffer: %s", err)
+ }
+ fileContentType := "text/plain; charset=utf-8"
+ if runtime.GOOS == "freebsd" {
+ fileContentType = "application/octet-stream"
+ }
+ wants := []msgContentTest{
+ {0, "Date:", false, true, false},
+ {1, "MIME-Version: 1.0", true, true, false},
+ {2, "Message-ID: <", false, true, false},
+ {2, ">", false, false, true},
+ {6, "From: ", true, true, false},
+ {7, "To: ", true, true, false},
+ {8, `Content-Type: multipart/mixed;`, true, true, false},
+ {9, ` boundary=`, false, true, false},
+ {10, "", true, false, false},
+ {11, "--", false, true, false},
+ {12, `Content-Type: multipart/related;`, true, true, false},
+ {13, ` boundary=`, false, true, false},
+ {14, "", true, false, false},
+ {15, "--", false, true, false},
+ {16, `Content-Type: multipart/alternative;`, true, true, false},
+ {17, ` boundary=`, false, true, false},
+ {18, "", true, false, false},
+ {19, "--", false, true, false},
+ {20, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {21, "Content-Type: text/plain; charset=UTF-8", true, true, false},
+ {22, "", true, false, false},
+ {23, "Testmail", true, true, false},
+ {24, "--", false, true, false},
+ {25, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {26, "Content-Type: text/html; charset=UTF-8", true, true, false},
+ {27, "", true, false, false},
+ {28, `HTML alternative part
`, true, true, false},
+ {29, "--", false, true, true},
+ {30, "", true, false, false},
+ {31, "--", false, true, false},
+ {32, `Content-Disposition: inline; filename="embed.txt"`, true, true, false},
+ {33, "Content-Id: ", true, true, false},
+ {34, "Content-Transfer-Encoding: base64", true, true, false},
+ {35, `Content-Type: ` + fileContentType + `; name="embed.txt"`, true, true, false},
+ {36, "", true, false, false},
+ {37, "VGhp", false, true, false},
+ {38, "", true, false, false},
+ {39, "--", false, true, true},
+ {40, "", true, false, false},
+ {41, "--", false, true, false},
+ {42, `Content-Disposition: attachment; filename="attachment.txt"`, true, true, false},
+ {43, "Content-Transfer-Encoding: base64", true, true, false},
+ {44, `Content-Type: ` + fileContentType + `; name="attachment.txt"`, true, true, false},
+ {45, "", true, false, false},
+ {46, "VGhp", false, true, false},
+ {47, "", true, false, false},
+ {48, "--", false, true, true},
+ }
+ checkMessageContent(t, buffer, wants)
+ })
+ t.Run("WriteTo Multipart plain body, alternative html, attachment, embed and S/MIME signed", func(t *testing.T) {
+ message := testMessage(t)
+ message.AddAlternativeString(TypeTextHTML, "HTML alternative part
")
+ message.AttachFile("testdata/attachment.txt")
+ message.EmbedFile("testdata/embed.txt")
+ keypair, err := getDummyKeyPairTLS()
+ if err != nil {
+ t.Fatalf("failed to load dummy key material: %s", err)
+ }
+ if err = message.SignWithTLSCertificate(keypair); err != nil {
+ t.Fatalf("failed to initialize S/MIME signing: %s", err)
+ }
+ buffer := bytes.NewBuffer(nil)
+ if _, err := message.WriteTo(buffer); err != nil {
+ t.Fatalf("failed to write message to buffer: %s", err)
+ }
+ fileContentType := "text/plain; charset=utf-8"
+ if runtime.GOOS == "freebsd" {
+ fileContentType = "application/octet-stream"
+ }
+ wants := []msgContentTest{
+ {0, "Date:", false, true, false},
+ {1, "MIME-Version: 1.0", true, true, false},
+ {2, "Message-ID: <", false, true, false},
+ {2, ">", false, false, true},
+ {6, "From: ", true, true, false},
+ {7, "To: ", true, true, false},
+ {
+ 8, `Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256;`, true,
+ true, false,
+ },
+ {9, ` boundary=`, false, true, false},
+ {10, "", true, false, false},
+ {11, "--", false, true, false},
+ {12, `Content-Type: multipart/mixed;`, true, true, false},
+ {13, ` boundary=`, false, true, false},
+ {14, "", true, false, false},
+ {15, "--", false, true, false},
+ {16, `Content-Type: multipart/related;`, true, true, false},
+ {17, ` boundary=`, false, true, false},
+ {18, "", true, false, false},
+ {19, "--", false, true, false},
+ {20, `Content-Type: multipart/alternative;`, true, true, false},
+ {21, ` boundary=`, false, true, false},
+ {22, "", true, false, false},
+ {23, "--", false, true, false},
+ {24, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {25, "Content-Type: text/plain; charset=UTF-8", true, true, false},
+ {26, "", true, false, false},
+ {27, "Testmail", true, true, false},
+ {28, "--", false, true, false},
+ {29, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {30, "Content-Type: text/html; charset=UTF-8", true, true, false},
+ {31, "", true, false, false},
+ {32, `HTML alternative part
`, true, true, false},
+ {33, "--", false, true, true},
+ {34, "", true, false, false},
+ {35, "--", false, true, false},
+ {36, `Content-Disposition: inline; filename="embed.txt"`, true, true, false},
+ {37, "Content-Id: ", true, true, false},
+ {38, "Content-Transfer-Encoding: base64", true, true, false},
+ {39, `Content-Type: ` + fileContentType + `; name="embed.txt"`, true, true, false},
+ {40, "", true, false, false},
+ {41, "VGhp", false, true, false},
+ {42, "", true, false, false},
+ {43, "--", false, true, true},
+ {44, "", true, false, false},
+ {45, "--", false, true, false},
+ {46, `Content-Disposition: attachment; filename="attachment.txt"`, true, true, false},
+ {47, "Content-Transfer-Encoding: base64", true, true, false},
+ {48, `Content-Type: ` + fileContentType + `; name="attachment.txt"`, true, true, false},
+ {49, "", true, false, false},
+ {50, "VGhp", false, true, false},
+ {51, "", true, false, false},
+ {52, "--", false, true, true},
+ {53, "", true, false, false},
+ {54, "--", false, true, false},
+ {55, "Content-Transfer-Encoding: base64", true, true, false},
+ {56, `Content-Type: application/pkcs7-signature; name="smime.p7s"`, true, true, false},
+ {57, "", true, false, false},
+ {58, "MII", false, true, false},
+ {121, "", true, false, false},
+ {122, "--", false, true, true},
+ }
+ checkMessageContent(t, buffer, wants)
+ })
}
func TestMsg_WriteToFile(t *testing.T) {
@@ -7177,6 +7440,35 @@ func hasSendmail() bool {
return false
}
+func checkMessageContent(t *testing.T, buffer *bytes.Buffer, wants []msgContentTest) {
+ t.Helper()
+ lines := strings.Split(buffer.String(), "\r\n")
+ for _, want := range wants {
+ if len(lines) <= want.line {
+ t.Errorf("expected line %d to be present, got: %d lines in total", want.line, len(lines)-1)
+ continue
+ }
+ if !strings.Contains(lines[want.line], want.data) {
+ t.Errorf("expected line %d to contain %q, got: %q", want.line, want.data, lines[want.line])
+ }
+ if want.exact {
+ if !strings.EqualFold(lines[want.line], want.data) {
+ t.Errorf("expected line %d to be exactly %q, got: %q", want.line, want.data, lines[want.line])
+ }
+ }
+ if want.dataIsPrefix {
+ if !strings.HasPrefix(lines[want.line], want.data) {
+ t.Errorf("expected line %d to start with %q, got: %q", want.line, want.data, lines[want.line])
+ }
+ }
+ if want.dataIsSuffix {
+ if !strings.HasSuffix(lines[want.line], want.data) {
+ t.Errorf("expected line %d to end with %q, got: %q", want.line, want.data, lines[want.line])
+ }
+ }
+ }
+}
+
// Fuzzing tests
func FuzzMsg_Subject(f *testing.F) {
f.Add("Testsubject")
diff --git a/quicksend_test.go b/quicksend_test.go
index d81e9dd8..5275ad89 100644
--- a/quicksend_test.go
+++ b/quicksend_test.go
@@ -97,34 +97,25 @@ func TestQuickSend(t *testing.T) {
t.Fatalf("failed to send email: %s", err)
}
+ wants := []msgContentTest{
+ {8, "STARTTLS", true, true, false},
+ {17, "AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk", true, true, false},
+ {21, "MAIL FROM: BODY=8BITMIME SMTPUTF8", true, true, false},
+ {23, "RCPT TO:", true, true, false},
+ {30, "Subject: " + subject, true, true, false},
+ {33, "From: ", true, true, false},
+ {34, "To: ", true, true, false},
+ {35, "Content-Transfer-Encoding: quoted-printable", true, true, false},
+ {36, "Content-Type: text/plain; charset=UTF-8", true, true, false},
+ {38, "This is a test body", true, true, false},
+ {39, "With multiple lines", true, true, false},
+ {40, "", true, true, false},
+ {41, "Best,", true, true, false},
+ {42, " The go-mail team", true, true, false},
+ }
props.BufferMutex.RLock()
- resp := strings.Split(echoBuffer.String(), "\r\n")
+ checkMessageContent(t, echoBuffer, wants)
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-Transfer-Encoding: quoted-printable"},
- {36, "Content-Type: text/plain; charset=UTF-8"},
- {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())