From da9092dfa8a3083e2f77fa3a4815145efb8d4a2c Mon Sep 17 00:00:00 2001 From: BluePsyduck Date: Sat, 15 Jun 2024 18:34:56 +0200 Subject: [PATCH 1/2] Added new RelatedWriter to write a multipart/related part into a mail. --- mail/attachment.go | 35 ++++++++++++++---- mail/attachment_test.go | 11 ++++++ mail/writer.go | 79 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/mail/attachment.go b/mail/attachment.go index 3fbbce26..928fe5fb 100644 --- a/mail/attachment.go +++ b/mail/attachment.go @@ -4,13 +4,8 @@ import ( "github.com/emersion/go-message" ) -// An AttachmentHeader represents an attachment's header. -type AttachmentHeader struct { - message.Header -} - -// Filename parses the attachment's filename. -func (h *AttachmentHeader) Filename() (string, error) { +// parseFilename parses the filename from the header. +func parseFilename(h message.Header) (string, error) { _, params, err := h.ContentDisposition() filename, ok := params["filename"] @@ -23,8 +18,34 @@ func (h *AttachmentHeader) Filename() (string, error) { return filename, err } +// An AttachmentHeader represents an attachment's header. +type AttachmentHeader struct { + message.Header +} + +// Filename parses the attachment's filename. +func (h *AttachmentHeader) Filename() (string, error) { + return parseFilename(h.Header) +} + // SetFilename formats the attachment's filename. func (h *AttachmentHeader) SetFilename(filename string) { dispParams := map[string]string{"filename": filename} h.SetContentDisposition("attachment", dispParams) } + +// An InlineAttachmentHeader represents an inlined attachment's header. +type InlineAttachmentHeader struct { + message.Header +} + +// Filename parses the attachment's filename. +func (h *InlineAttachmentHeader) Filename() (string, error) { + return parseFilename(h.Header) +} + +// SetFilename formats the attachment's filename. +func (h *InlineAttachmentHeader) SetFilename(filename string) { + dispParams := map[string]string{"filename": filename} + h.SetContentDisposition("inline", dispParams) +} diff --git a/mail/attachment_test.go b/mail/attachment_test.go index 9424b492..1c70ef31 100644 --- a/mail/attachment_test.go +++ b/mail/attachment_test.go @@ -49,3 +49,14 @@ func TestAttachmentHeader_Filename_encoded(t *testing.T) { t.Errorf("Expected filename to be %q but got %q", "", filename) } } + +func TestInlineAttachmentHeader_Filename(t *testing.T) { + var h mail.InlineAttachmentHeader + h.Set("Content-Disposition", "inline; filename=note.txt") + + if filename, err := h.Filename(); err != nil { + t.Error("Expected no error while parsing filename, got:", err) + } else if filename != "note.txt" { + t.Errorf("Expected filename to be %q but got %q", "note.txt", filename) + } +} diff --git a/mail/writer.go b/mail/writer.go index 6e6a0d24..d6c2efbe 100644 --- a/mail/writer.go +++ b/mail/writer.go @@ -33,6 +33,16 @@ func initAttachmentHeader(h *AttachmentHeader) { } } +func initInlineAttachmentHeader(h *InlineAttachmentHeader) { + disp, _, _ := h.ContentDisposition() + if disp != "inline" { + h.Set("Content-Disposition", "inline") + } + if !h.Has("Content-Transfer-Encoding") { + h.Set("Content-Transfer-Encoding", "base64") + } +} + // A Writer writes a mail message. A mail message contains one or more text // parts and zero or more attachments. type Writer struct { @@ -77,6 +87,20 @@ func CreateSingleInlineWriter(w io.Writer, header Header) (io.WriteCloser, error return message.CreateWriter(w, header.Header) } +// CreateRelatedWriter writes a mail header to w. The mail will contain an +// inline part and any inline attachments. Non-inline attachments cannot be added. +func CreateRelatedWriter(w io.Writer, header Header) (*RelatedWriter, error) { + header = header.Copy() // don't modify the caller's view + header.Set("Content-Type", "multipart/related") + + mw, err := message.CreateWriter(w, header.Header) + if err != nil { + return nil, err + } + + return &RelatedWriter{mw}, nil +} + // CreateInline creates a InlineWriter. One or more parts representing the same // text in different formats can be written to a InlineWriter. func (w *Writer) CreateInline() (*InlineWriter, error) { @@ -90,6 +114,20 @@ func (w *Writer) CreateInline() (*InlineWriter, error) { return &InlineWriter{mw}, nil } +// CreateRelated created a RelatedWriter. Inline attachments and one or more +// parts representing the same text in different format can be written to a +// RelatedWriter. +func (w *Writer) CreateRelated() (*RelatedWriter, error) { + var h message.Header + h.Set("Content-Type", "multipart/related") + + mw, err := w.mw.CreatePart(h) + if err != nil { + return nil, err + } + return &RelatedWriter{mw}, nil +} + // CreateSingleInline creates a new single text part with the provided header. // The body of the part should be written to the returned io.WriteCloser. Only // one single text part should be written, use CreateInline if you want multiple @@ -130,3 +168,44 @@ func (w *InlineWriter) CreatePart(h InlineHeader) (io.WriteCloser, error) { func (w *InlineWriter) Close() error { return w.mw.Close() } + +// RelatedWriter write a mail message with inline attachments and text parts. +type RelatedWriter struct { + mw *message.Writer +} + +// CreateInline creates a InlineWriter. One or more parts representing the same +// text in different formats can be written to a InlineWriter. +func (w *RelatedWriter) CreateInline() (*InlineWriter, error) { + var h message.Header + h.Set("Content-Type", "multipart/alternative") + + mw, err := w.mw.CreatePart(h) + if err != nil { + return nil, err + } + return &InlineWriter{mw}, nil +} + +// CreateSingleInline creates a new single text part with the provided header. +// The body of the part should be written to the returned io.WriteCloser. Only +// one single text part should be written, use CreateInline if you want multiple +// text parts. +func (w *RelatedWriter) CreateSingleInline(h InlineHeader) (io.WriteCloser, error) { + h = InlineHeader{h.Header.Copy()} // don't modify the caller's view + initInlineHeader(&h) + return w.mw.CreatePart(h.Header) +} + +// CreateInlineAttachment creates a new inline attachment with the provided header. +// The body of the part should be written to the returned io.WriteCloser. +func (w *RelatedWriter) CreateInlineAttachment(h InlineAttachmentHeader) (io.WriteCloser, error) { + h = InlineAttachmentHeader{h.Header.Copy()} // don't modify the caller's view + initInlineAttachmentHeader(&h) + return w.mw.CreatePart(h.Header) +} + +// Close finishes the RelatedWriter. +func (w *RelatedWriter) Close() error { + return w.mw.Close() +} From 95dbd4c209fb789d9050f29daaa2155fde3b8351 Mon Sep 17 00:00:00 2001 From: BluePsyduck Date: Sat, 15 Jun 2024 22:52:53 +0200 Subject: [PATCH 2/2] Added AlternativeWriter replacing the InlineWriter, deprecated the latter. --- mail/writer.go | 82 ++++++++++++++++++++++++++++++++++++--------- mail/writer_test.go | 4 +-- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/mail/writer.go b/mail/writer.go index d6c2efbe..3cbc52b0 100644 --- a/mail/writer.go +++ b/mail/writer.go @@ -62,9 +62,25 @@ func CreateWriter(w io.Writer, header Header) (*Writer, error) { return &Writer{mw}, nil } +// CreateAlternativeWriter writes a mail header to w. The mail will contain an +// inline part, allowing to represent the same text in different formats. +// Attachments cannot be included. +func CreateAlternativeWriter(w io.Writer, header Header) (*AlternativeWriter, error) { + header = header.Copy() // don't modify the caller's view + header.Set("Content-Type", "multipart/alternative") + + mw, err := message.CreateWriter(w, header.Header) + if err != nil { + return nil, err + } + + return &AlternativeWriter{mw}, nil +} + // CreateInlineWriter writes a mail header to w. The mail will contain an // inline part, allowing to represent the same text in different formats. // Attachments cannot be included. +// Deprecated: Use CreateAlternativeWriter and the returned AlternativeWriter instead. func CreateInlineWriter(w io.Writer, header Header) (*InlineWriter, error) { header = header.Copy() // don't modify the caller's view header.Set("Content-Type", "multipart/alternative") @@ -77,6 +93,20 @@ func CreateInlineWriter(w io.Writer, header Header) (*InlineWriter, error) { return &InlineWriter{mw}, nil } +// CreateRelatedWriter writes a mail header to w. The mail will contain an +// inline part and any inline attachments. Non-inline attachments cannot be added. +func CreateRelatedWriter(w io.Writer, header Header) (*RelatedWriter, error) { + header = header.Copy() // don't modify the caller's view + header.Set("Content-Type", "multipart/related") + + mw, err := message.CreateWriter(w, header.Header) + if err != nil { + return nil, err + } + + return &RelatedWriter{mw}, nil +} + // CreateSingleInlineWriter writes a mail header to w. The mail will contain a // single inline part. The body of the part should be written to the returned // io.WriteCloser. Only one single inline part should be written, use @@ -87,22 +117,24 @@ func CreateSingleInlineWriter(w io.Writer, header Header) (io.WriteCloser, error return message.CreateWriter(w, header.Header) } -// CreateRelatedWriter writes a mail header to w. The mail will contain an -// inline part and any inline attachments. Non-inline attachments cannot be added. -func CreateRelatedWriter(w io.Writer, header Header) (*RelatedWriter, error) { - header = header.Copy() // don't modify the caller's view - header.Set("Content-Type", "multipart/related") +// ------------------------------------------------------------------------------------------------------------------ // - mw, err := message.CreateWriter(w, header.Header) +// CreateAlternative creates an AlternativeWriter. One or more parts representing the same +// text in different formats can be written to an AlternativeWriter. +func (w *Writer) CreateAlternative() (*AlternativeWriter, error) { + var h message.Header + h.Set("Content-Type", "multipart/alternative") + + mw, err := w.mw.CreatePart(h) if err != nil { return nil, err } - - return &RelatedWriter{mw}, nil + return &AlternativeWriter{mw}, nil } // CreateInline creates a InlineWriter. One or more parts representing the same // text in different formats can be written to a InlineWriter. +// Deprecated: Use CreateAlternative() and the AlternativeWriter instead. func (w *Writer) CreateInline() (*InlineWriter, error) { var h message.Header h.Set("Content-Type", "multipart/alternative") @@ -130,8 +162,8 @@ func (w *Writer) CreateRelated() (*RelatedWriter, error) { // CreateSingleInline creates a new single text part with the provided header. // The body of the part should be written to the returned io.WriteCloser. Only -// one single text part should be written, use CreateInline if you want multiple -// text parts. +// one single text part should be written, use CreateAlternative if you want +// multiple text parts. func (w *Writer) CreateSingleInline(h InlineHeader) (io.WriteCloser, error) { h = InlineHeader{h.Header.Copy()} // don't modify the caller's view initInlineHeader(&h) @@ -151,13 +183,33 @@ func (w *Writer) Close() error { return w.mw.Close() } +// AlternativeWriter writes a mail message's text. +type AlternativeWriter struct { + mw *message.Writer +} + +// CreatePart creates a new text part with the provided header. The body of the +// part should be written to the returned io.WriteCloser. +func (w *AlternativeWriter) CreatePart(h InlineHeader) (io.WriteCloser, error) { + h = InlineHeader{h.Header.Copy()} // don't modify the caller's view + initInlineHeader(&h) + return w.mw.CreatePart(h.Header) +} + +// Close finishes the AlternativeWriter. +func (w *AlternativeWriter) Close() error { + return w.mw.Close() +} + // InlineWriter writes a mail message's text. +// Deprecated: Use AlternativeWriter instead. type InlineWriter struct { mw *message.Writer } // CreatePart creates a new text part with the provided header. The body of the // part should be written to the returned io.WriteCloser. +// Deprecated: Use AlternativeWriter and its CreatePart instead. func (w *InlineWriter) CreatePart(h InlineHeader) (io.WriteCloser, error) { h = InlineHeader{h.Header.Copy()} // don't modify the caller's view initInlineHeader(&h) @@ -174,9 +226,9 @@ type RelatedWriter struct { mw *message.Writer } -// CreateInline creates a InlineWriter. One or more parts representing the same -// text in different formats can be written to a InlineWriter. -func (w *RelatedWriter) CreateInline() (*InlineWriter, error) { +// CreateAlternative creates an AlternativeWriter. One or more parts representing the same +// text in different formats can be written to the AlternativeWriter. +func (w *RelatedWriter) CreateAlternative() (*AlternativeWriter, error) { var h message.Header h.Set("Content-Type", "multipart/alternative") @@ -184,12 +236,12 @@ func (w *RelatedWriter) CreateInline() (*InlineWriter, error) { if err != nil { return nil, err } - return &InlineWriter{mw}, nil + return &AlternativeWriter{mw}, nil } // CreateSingleInline creates a new single text part with the provided header. // The body of the part should be written to the returned io.WriteCloser. Only -// one single text part should be written, use CreateInline if you want multiple +// one single text part should be written, use CreateAlternative if you want multiple // text parts. func (w *RelatedWriter) CreateSingleInline(h InlineHeader) (io.WriteCloser, error) { h = InlineHeader{h.Header.Copy()} // don't modify the caller's view diff --git a/mail/writer_test.go b/mail/writer_test.go index edaeabaa..79bd9b8c 100644 --- a/mail/writer_test.go +++ b/mail/writer_test.go @@ -29,7 +29,7 @@ func ExampleWriter() { } // Create a text part - tw, err := mw.CreateInline() + tw, err := mw.CreateAlternative() if err != nil { log.Fatal(err) } @@ -70,7 +70,7 @@ func TestWriter(t *testing.T) { } // Create a text part - tw, err := mw.CreateInline() + tw, err := mw.CreateAlternative() if err != nil { t.Fatal(err) }