Open BluePsyduck opened 6 months ago
Aren't multipart/*
the same in terms of writer? ALternative implementation:
InlineWriter
to common MultipartWriter
.Writer.CreateInline()
a common Writer.CreateMultipart(contentType string)
.Writer.CreateRelated()
only calling Writer.CreateMultipart("multipart/related")
.Writer.CreateInline()
to only call Writer.CreateMultipart("multipart/alternative")
.Writer.CreateAlternative()
only calling Writer.CreateMultipart("multipart/alternative")
.Writer.CreateInline()
to only call Writer.CreateAlternative()
and deprecate.Need also some API to allow nesting MultipartWriter
.
Just in case, I temporary end up with such replacement:
package mailwriter
import (
"io"
"mime"
"strings"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
)
func initInlineContentTransferEncoding(h *message.Header) {
if !h.Has("Content-Transfer-Encoding") {
t, _, _ := h.ContentType()
if strings.HasPrefix(t, "text/") {
h.Set("Content-Transfer-Encoding", "quoted-printable")
} else {
h.Set("Content-Transfer-Encoding", "base64")
}
}
}
func initInlineHeader(h *mail.InlineHeader) {
disp, _, _ := mime.ParseMediaType(h.Get("Content-Disposition"))
if disp != "inline" {
h.Set("Content-Disposition", "inline")
}
initInlineContentTransferEncoding(&h.Header)
}
func initAttachmentHeader(h *mail.AttachmentHeader) {
disp, _, _ := h.ContentDisposition()
if disp != "attachment" {
h.Set("Content-Disposition", "attachment")
}
if !h.Has("Content-Transfer-Encoding") {
h.Set("Content-Transfer-Encoding", "base64")
}
}
type Writer struct {
mw *message.Writer
}
func CreateWriter(w io.Writer, header mail.Header) (*Writer, error) {
return createMultipart(w, "multipart/mixed", header)
}
func CreateAlternativeWriter(w io.Writer, header mail.Header) (*Writer, error) {
return createMultipart(w, "multipart/alternative", header)
}
func CreateRelatedWriter(w io.Writer, header mail.Header) (*Writer, error) {
return createMultipart(w, "multipart/related", header)
}
func createMultipart(w io.Writer, contentType string, header mail.Header) (*Writer, error) {
header = header.Copy() // don't modify the caller's view
header.Set("Content-Type", contentType)
mw, err := message.CreateWriter(w, header.Header)
if err != nil {
return nil, err
}
return &Writer{mw}, nil
}
func CreateSingleInlineWriter(w io.Writer, header mail.Header) (io.WriteCloser, error) {
header = header.Copy() // don't modify the caller's view
initInlineContentTransferEncoding(&header.Header)
return message.CreateWriter(w, header.Header)
}
func (w *Writer) CreateAlternative() (*Writer, error) {
return w.createMultipart("multipart/alternative")
}
func (w *Writer) CreateRelated() (*Writer, error) {
return w.createMultipart("multipart/related")
}
func (w *Writer) createMultipart(contentType string) (*Writer, error) {
var h message.Header
h.Set("Content-Type", contentType)
mw, err := w.mw.CreatePart(h)
if err != nil {
return nil, err
}
return &Writer{mw}, nil
}
func (w *Writer) CreateInlinePart(h mail.InlineHeader) (io.WriteCloser, error) {
h = mail.InlineHeader{h.Header.Copy()} // don't modify the caller's view
initInlineHeader(&h)
return w.mw.CreatePart(h.Header)
}
func (w *Writer) CreateAttachment(h mail.AttachmentHeader) (io.WriteCloser, error) {
h = mail.AttachmentHeader{h.Header.Copy()} // don't modify the caller's view
initAttachmentHeader(&h)
return w.mw.CreatePart(h.Header)
}
func (w *Writer) Close() error {
return w.mw.Close()
}
Thank you for your responses.
While the multipart writers might be the same (I didn't check the RFC for what is allowed, I find them hard to understand...), I think the writers should stay separate to make it easier to understand how the common usecases of mails should be structured. With everything in the Writer, you have no restrictions anymore where to create which parts of the mail, e.g. now you can put attachments right into the alternative parts, which will most likely not work as expected. That's why I will stay with my initial thought of having a new RelatedWriter, so the methods on the different writers dictate what is possible to add at that point.
What I agree with you is the kind-of misleading naming of InlineWriter
and its corresponding functions. I will change them to AlternativeWriter
(better matching the new RelatedWriter
) and mark the old wordings as deprecated.
Feature request: Support Content-Type "multipart/related" to allow inline attachments
An inline attachment (or embedded attachment) is e.g. an image attached to the mail, which is referenced and inlined directly into the HTML part of the mail. The difference to normal attachments is that an inline attachment will not be listed in the attachment list of the mail (if the client supports it).
To support inline attachments, the content-type "multipart/related" is required, which will contain the inline parts (either a single one or via multipart/alternative), as well as the embedded attachments using "content-disposition: inline".
There are two use cases which have to be accounted for:
Use case 1: Only inline attachments
When only using inline attachments, the top-level content-type is "multipart/related". The MIME message will have the following structure:
Source: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/92ce322f-efa1-48c5-828a-543063fa9db8
Use case 2: Inline attachments and non-inline attachments
When mixing inline attachments with non-inline ones, the top-level content-type is "multipart/mixed", and one of its parts will be "multipart/related". The MIME message will have the following structure:
Source: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/7a08211a-760a-41af-8cab-0acf462c4094
Proposal
To cover these use cases, I propose the following changes within the mail package. (I will also attempt to implement this myself and open a pull request in the next days.)
RelatedWriter
similar to the existingWriter
, with the methods CreateInline(), CreateSingleInline() and CreateInlineAttachment() to build up the parts of "multipart/related".func CreateRelatedWriter(w io.Writer, header Header) (*RelatedWriter, error)
to create a writer covering use-case 1, where the top-level content-type is "multipart/related".func (w *Writer) CreateRelated() (*RelatedWriter, error)
to the existingWriter
to support use-case 2 with mixed the attachments.Any comments are welcome, especially whether the naming of "RelatedWriter" is good enough for this.