diff --git a/client_test.go b/client_test.go index 8408035..1ba0fc9 100644 --- a/client_test.go +++ b/client_test.go @@ -3528,9 +3528,9 @@ func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData { } // testMessage configures and returns a new email message for testing, initializing it with valid sender and recipient. -func testMessage(t *testing.T) *Msg { +func testMessage(t *testing.T, opts ...MsgOption) *Msg { t.Helper() - message := NewMsg() + message := NewMsg(opts...) if message == nil { t.Fatal("failed to create new message") } diff --git a/msgwriter_test.go b/msgwriter_test.go index a41e5d3..babb52a 100644 --- a/msgwriter_test.go +++ b/msgwriter_test.go @@ -6,151 +6,293 @@ package mail import ( "bytes" + "errors" "fmt" - "io" "mime" "strings" "testing" "time" ) -// brokenWriter implements a broken writer for io.Writer testing -type brokenWriter struct { - io.Writer -} - -// Write implements the io.Writer interface but intentionally returns an error at -// any time -func (bw *brokenWriter) Write([]byte) (int, error) { - return 0, fmt.Errorf("intentionally failed") -} - -// TestMsgWriter_Write tests the WriteTo() method of the msgWriter func TestMsgWriter_Write(t *testing.T) { - bw := &brokenWriter{} - mw := &msgWriter{writer: bw, charset: CharsetUTF8, encoder: mime.QEncoding} - _, err := mw.Write([]byte("test")) - if err == nil { - t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't") - } - - // Also test the part when a previous error happened - mw.err = fmt.Errorf("broken") - _, err = mw.Write([]byte("test")) - if err == nil { - t.Errorf("msgWriter WriteTo() with brokenWriter should fail, but didn't") - } -} - -// TestMsgWriter_writeMsg tests the writeMsg method of the msgWriter -func TestMsgWriter_writeMsg(t *testing.T) { - m := NewMsg() - _ = m.From(`"Toni Tester" `) - _ = m.To(`"Toni Receiver" `) - m.Subject("This is a subject") - m.SetBulk() - now := time.Now() - m.SetDateWithValue(now) - m.SetMessageIDWithValue("message@id.com") - m.SetBodyString(TypeTextPlain, "This is the body") - m.AddAlternativeString(TypeTextHTML, "This is the alternative body") - buf := bytes.Buffer{} - mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} - mw.writeMsg(m) - ms := buf.String() - - var ea []string - if !strings.Contains(ms, `MIME-Version: 1.0`) { - ea = append(ea, "MIME-Version") - } - if !strings.Contains(ms, fmt.Sprintf("Date: %s", now.Format(time.RFC1123Z))) { - ea = append(ea, "Date") - } - if !strings.Contains(ms, `Message-ID: `) { - ea = append(ea, "Message-ID") - } - if !strings.Contains(ms, `Precedence: bulk`) { - ea = append(ea, "Precedence") - } - if !strings.Contains(ms, `Subject: This is a subject`) { - ea = append(ea, "Subject") - } - if !strings.Contains(ms, `User-Agent: go-mail v`) { - ea = append(ea, "User-Agent") - } - if !strings.Contains(ms, `X-Mailer: go-mail v`) { - ea = append(ea, "X-Mailer") - } - if !strings.Contains(ms, `From: "Toni Tester" `) { - ea = append(ea, "From") - } - if !strings.Contains(ms, `To: "Toni Receiver" `) { - ea = append(ea, "To") - } - if !strings.Contains(ms, `Content-Type: text/plain; charset=UTF-8`) { - ea = append(ea, "Content-Type") - } - if !strings.Contains(ms, `Content-Transfer-Encoding: quoted-printable`) { - ea = append(ea, "Content-Transfer-Encoding") - } - if !strings.Contains(ms, "\r\n\r\nThis is the body") { - ea = append(ea, "Message body") - } - - pl := m.GetParts() - if len(pl) <= 0 { - t.Errorf("expected multiple parts but got none") - return - } - if len(pl) == 2 { - ap := pl[1] - ap.SetCharset(CharsetISO88591) - } - buf.Reset() - mw.writeMsg(m) - ms = buf.String() - if !strings.Contains(ms, "\r\n\r\nThis is the alternative body") { - ea = append(ea, "Message alternative body") - } - if !strings.Contains(ms, `Content-Type: text/html; charset=ISO-8859-1`) { - ea = append(ea, "alternative body charset") - } - - if len(ea) > 0 { - em := "writeMsg() failed. The following errors occurred:\n" - for e := range ea { - em += fmt.Sprintf("* incorrect %q field", ea[e]) + t.Run("msgWriter writes to memory for all charsets", func(t *testing.T) { + for _, tt := range charsetTests { + t.Run(tt.name, func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter := &msgWriter{ + writer: buffer, + charset: tt.value, + encoder: mime.QEncoding, + } + _, err := msgwriter.Write([]byte("test")) + if err != nil { + t.Errorf("msgWriter failed to write: %s", err) + } + }) } - em += fmt.Sprintf("\n\nFull message:\n%s", ms) - t.Error(em) - } + }) + t.Run("msgWriter writes to memory for all encodings", func(t *testing.T) { + for _, tt := range encodingTests { + t.Run(tt.name, func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter := &msgWriter{ + writer: buffer, + charset: CharsetUTF8, + encoder: getEncoder(tt.value), + } + _, err := msgwriter.Write([]byte("test")) + if err != nil { + t.Errorf("msgWriter failed to write: %s", err) + } + }) + } + }) + t.Run("msgWriter should fail on write", func(t *testing.T) { + msgwriter := &msgWriter{ + writer: failReadWriteSeekCloser{}, + charset: CharsetUTF8, + encoder: getEncoder(EncodingQP), + } + _, err := msgwriter.Write([]byte("test")) + if err == nil { + t.Fatalf("msgWriter was supposed to fail on write") + } + }) + t.Run("msgWriter should fail on previous error", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter := &msgWriter{ + writer: buffer, + charset: CharsetUTF8, + encoder: getEncoder(EncodingQP), + } + _, err := msgwriter.Write([]byte("test")) + if err != nil { + t.Errorf("msgWriter failed to write: %s", err) + } + msgwriter.err = errors.New("intentionally failed") + _, err = msgwriter.Write([]byte("test2")) + if err == nil { + t.Fatalf("msgWriter was supposed to fail on second write") + } + }) } -// TestMsgWriter_writeMsg_PGP tests the writeMsg method of the msgWriter with PGP types set -func TestMsgWriter_writeMsg_PGP(t *testing.T) { - m := NewMsg(WithPGPType(PGPEncrypt)) - _ = m.From(`"Toni Tester" `) - _ = m.To(`"Toni Receiver" `) - m.Subject("This is a subject") - m.SetBodyString(TypeTextPlain, "This is the body") - buf := bytes.Buffer{} - mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} - mw.writeMsg(m) - ms := buf.String() - if !strings.Contains(ms, `encrypted; protocol="application/pgp-encrypted"`) { - t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") +func TestMsgWriter_writeMsg(t *testing.T) { + msgwriter := &msgWriter{ + charset: CharsetUTF8, + encoder: getEncoder(EncodingQP), } + t.Run("msgWriter writes a simple message", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + now := time.Now() + msgwriter.writer = buffer + message := testMessage(t) + message.SetDateWithValue(now) + message.SetMessageIDWithValue("message@id.com") + message.SetBulk() + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } - m = NewMsg(WithPGPType(PGPSignature)) - _ = m.From(`"Toni Tester" `) - _ = m.To(`"Toni Receiver" `) - m.Subject("This is a subject") - m.SetBodyString(TypeTextPlain, "This is the body") - buf = bytes.Buffer{} - mw = &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding} - mw.writeMsg(m) - ms = buf.String() - if !strings.Contains(ms, `signed; protocol="application/pgp-signature"`) { - t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output") - } + var incorrectFields []string + if !strings.Contains(buffer.String(), "MIME-Version: 1.0\r\n") { + incorrectFields = append(incorrectFields, "MIME-Version") + } + if !strings.Contains(buffer.String(), fmt.Sprintf("Date: %s\r\n", now.Format(time.RFC1123Z))) { + incorrectFields = append(incorrectFields, "Date") + } + if !strings.Contains(buffer.String(), "Message-ID: \r\n") { + incorrectFields = append(incorrectFields, "Message-ID") + } + if !strings.Contains(buffer.String(), "Precedence: bulk\r\n") { + incorrectFields = append(incorrectFields, "Precedence") + } + if !strings.Contains(buffer.String(), "X-Auto-Response-Suppress: All\r\n") { + incorrectFields = append(incorrectFields, "X-Auto-Response-Suppress") + } + if !strings.Contains(buffer.String(), "Subject: Testmail\r\n") { + incorrectFields = append(incorrectFields, "Subject") + } + if !strings.Contains(buffer.String(), "User-Agent: go-mail v") { + incorrectFields = append(incorrectFields, "User-Agent") + } + if !strings.Contains(buffer.String(), "X-Mailer: go-mail v") { + incorrectFields = append(incorrectFields, "X-Mailer") + } + if !strings.Contains(buffer.String(), `From: <`+TestSenderValid+`>`) { + incorrectFields = append(incorrectFields, "From") + } + if !strings.Contains(buffer.String(), `To: <`+TestRcptValid+`>`) { + incorrectFields = append(incorrectFields, "From") + } + if !strings.Contains(buffer.String(), "Content-Type: text/plain; charset=UTF-8\r\n") { + incorrectFields = append(incorrectFields, "Content-Type") + } + if !strings.Contains(buffer.String(), "Content-Transfer-Encoding: quoted-printable\r\n") { + incorrectFields = append(incorrectFields, "Content-Transfer-Encoding") + } + if !strings.HasSuffix(buffer.String(), "\r\n\r\nTestmail") { + incorrectFields = append(incorrectFields, "Message body") + } + if len(incorrectFields) > 0 { + t.Fatalf("msgWriter failed to write correct fields: %s - mail: %s", + strings.Join(incorrectFields, ", "), buffer.String()) + } + }) + t.Run("msgWriter with no from address uses envelope from", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := NewMsg() + if message == nil { + t.Fatal("failed to create new message") + } + if err := message.EnvelopeFrom(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.SetBodyString(TypeTextPlain, "Testmail") + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "From: <"+TestSenderValid+">") { + t.Errorf("expected envelope from address as from address, got: %s", buffer.String()) + } + }) + t.Run("msgWriter with no from address or envelope from", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := NewMsg() + if message == nil { + t.Fatal("failed to create new message") + } + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if strings.Contains(buffer.String(), "From:") { + t.Errorf("expected no from address, got: %s", buffer.String()) + } + }) + t.Run("msgWriter writes a multipart/mixed message", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t, WithBoundary("testboundary")) + message.AttachFile("testdata/attachment.txt") + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "Content-Type: multipart/mixed") { + t.Errorf("expected multipart/mixed, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary\r\n") { + t.Errorf("expected boundary, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary--") { + t.Errorf("expected end boundary, got: %s", buffer.String()) + } + }) + t.Run("msgWriter writes a multipart/related message", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t, WithBoundary("testboundary")) + message.EmbedFile("testdata/embed.txt") + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "Content-Type: multipart/related") { + t.Errorf("expected multipart/related, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary\r\n") { + t.Errorf("expected boundary, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary--") { + t.Errorf("expected end boundary, got: %s", buffer.String()) + } + }) + t.Run("msgWriter writes a multipart/alternative message", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t, WithBoundary("testboundary")) + message.AddAlternativeString(TypeTextHTML, "

Testmail

") + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "Content-Type: multipart/alternative") { + t.Errorf("expected multipart/alternative, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary\r\n") { + t.Errorf("expected boundary, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary--") { + t.Errorf("expected end boundary, got: %s", buffer.String()) + } + }) + t.Run("msgWriter writes a application/pgp-encrypted message", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t, WithPGPType(PGPEncrypt), WithBoundary("testboundary")) + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "Content-Type: multipart/encrypted") { + t.Errorf("expected multipart/encrypted, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary\r\n") { + t.Errorf("expected boundary, got: %s", buffer.String()) + } + }) + t.Run("msgWriter writes a application/pgp-signature message", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t, WithPGPType(PGPSignature), WithBoundary("testboundary")) + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "Content-Type: multipart/signed") { + t.Errorf("expected multipart/signed, got: %s", buffer.String()) + } + if !strings.Contains(buffer.String(), "--testboundary\r\n") { + t.Errorf("expected boundary, got: %s", buffer.String()) + } + }) + t.Run("msgWriter should ignore NoPGP", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter.writer = buffer + message := testMessage(t, WithBoundary("testboundary")) + message.pgptype = 9 + msgwriter.writeMsg(message) + if msgwriter.err != nil { + t.Errorf("msgWriter failed to write: %s", msgwriter.err) + } + if !strings.Contains(buffer.String(), "--testboundary\r\n") { + t.Errorf("expected boundary, got: %s", buffer.String()) + } + }) +} + +func TestMsgWriter_writePreformattedGenHeader(t *testing.T) { + t.Run("message with no preformatted headerset", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + msgwriter := &msgWriter{ + writer: buffer, + charset: CharsetUTF8, + encoder: getEncoder(EncodingQP), + } + message := testMessage(t) + message.SetGenHeaderPreformatted(HeaderContentID, "This is a content id") + msgwriter.writeMsg(message) + if !strings.Contains(buffer.String(), "Content-ID: This is a content id\r\n") { + t.Errorf("expected preformatted header, got: %s", buffer.String()) + } + }) }