Skip to content

Commit

Permalink
Merge pull request #422 from wneessen/chore/420_improve-tests-for-mul…
Browse files Browse the repository at this point in the history
…tipart-messages

chore: improve tests for multipart messages
  • Loading branch information
wneessen authored Jan 19, 2025
2 parents f781a50 + 267da1a commit 7e0e484
Show file tree
Hide file tree
Showing 3 changed files with 310 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
292 changes: 292 additions & 0 deletions msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, "<p>HTML alternative</p>")
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, `<p>HTML alternative</p>`, 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, "<p>HTML main part</p>")
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, "<p>HTML main part</p>", 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, "<p>HTML alternative part</p>")
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: <[email protected]>", true, true, false},
{7, "To: <[email protected]>", 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, `<p>HTML alternative part</p>`, 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: <embed.txt>", 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, "<p>HTML alternative part</p>")
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: <[email protected]>", true, true, false},
{7, "To: <[email protected]>", 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, `<p>HTML alternative part</p>`, 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: <embed.txt>", 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) {
Expand Down Expand Up @@ -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")
Expand Down
43 changes: 17 additions & 26 deletions quicksend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:<[email protected]> BODY=8BITMIME SMTPUTF8", true, true, false},
{23, "RCPT TO:<[email protected]>", true, true, false},
{30, "Subject: " + subject, true, true, false},
{33, "From: <[email protected]>", true, true, false},
{34, "To: <[email protected]>", 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:<[email protected]> BODY=8BITMIME SMTPUTF8"},
{23, "RCPT TO:<[email protected]>"},
{30, "Subject: " + subject},
{33, "From: <[email protected]>"},
{34, "To: <[email protected]>"},
{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())
Expand Down

0 comments on commit 7e0e484

Please sign in to comment.