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())