satellite/email: add delimiter to close the last part of Multipart emails
The existing implementation doesn't send proper rfc1341 compatible mails according to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html as the closing `--boundary_id--` is missing from our mails. (see 7.2.1 from the standard). Mailservers which are not very flexible, deny the mails. Example mail log: (see the missing boundary delimiter at the end). ``` Subject: Activate your email From: "Storj DCS - EU1" <noreply@eu1.storj.io> To: "..." <...> ... --26d7220f6f1c9f6fb47b535319eb15dce513bb6e1d941a0efddf25e96712 Content-Type: text/html; charset=UTF-8 .... </body> </html> . smtp: DATA error {"msg_id":"6d82c9e3","reason":"unexpected EOF"} smtp: 554 5.0.0 Internal server error (msg ID = 6d82c9e3) ``` This patch moves all the Multipart writing to a function to make sure that the `wr.Close()` (which writes out the last part) is executed BEFORE `body.Bytes()` (defer added it AFTER `body.Bytes()` was calculated) Change-Id: I8f18fc81b1857b646470eab32e73d6cbdc50d2ad
This commit is contained in:
parent
0d03473e00
commit
2ae78db660
@ -66,44 +66,9 @@ func (msg *Message) Bytes() (data []byte, err error) {
|
|||||||
switch {
|
switch {
|
||||||
// multipart upload
|
// multipart upload
|
||||||
case len(msg.Parts) > 0:
|
case len(msg.Parts) > 0:
|
||||||
wr := multipart.NewWriter(&body)
|
err = msg.writeMultipart(&body)
|
||||||
defer func() { err = errs.Combine(err, wr.Close()) }()
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
fmt.Fprintf(&body, "Content-Type: multipart/alternative;")
|
|
||||||
fmt.Fprintf(&body, "\tboundary=\"%v\"\r\n", wr.Boundary())
|
|
||||||
fmt.Fprintf(&body, "\r\n")
|
|
||||||
|
|
||||||
var sub io.Writer
|
|
||||||
|
|
||||||
if len(msg.PlainText) > 0 {
|
|
||||||
sub, err := wr.CreatePart(textproto.MIMEHeader{
|
|
||||||
"Content-Type": []string{"text/plain; charset=UTF-8; format=flowed"},
|
|
||||||
"Content-Transfer-Encoding": []string{"quoted-printable"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, Error.Wrap(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := quotedprintable.NewWriter(sub)
|
|
||||||
defer func() { err = errs.Combine(err, enc.Close()) }()
|
|
||||||
|
|
||||||
_, err = enc.Write([]byte(msg.PlainText))
|
|
||||||
if err != nil {
|
|
||||||
return nil, Error.Wrap(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, part := range msg.Parts {
|
|
||||||
header := textproto.MIMEHeader{"Content-Type": []string{mime.QEncoding.Encode("utf-8", part.Type)}}
|
|
||||||
if part.Encoding != "" {
|
|
||||||
header["Content-Transfer-Encoding"] = []string{mime.QEncoding.Encode("utf-8", part.Encoding)}
|
|
||||||
}
|
|
||||||
if part.Disposition != "" {
|
|
||||||
header["Content-Disposition"] = []string{mime.QEncoding.Encode("utf-8", part.Disposition)}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, _ = wr.CreatePart(header)
|
|
||||||
fmt.Fprint(sub, part.Content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback if there are no parts, write PlainText with appropriate Content-Type
|
// fallback if there are no parts, write PlainText with appropriate Content-Type
|
||||||
@ -122,6 +87,49 @@ func (msg *Message) Bytes() (data []byte, err error) {
|
|||||||
return tocrlf(body.Bytes()), nil
|
return tocrlf(body.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (msg *Message) writeMultipart(body *bytes.Buffer) (err error) {
|
||||||
|
wr := multipart.NewWriter(body)
|
||||||
|
defer func() { err = errs.Combine(err, wr.Close()) }()
|
||||||
|
|
||||||
|
fmt.Fprintf(body, "Content-Type: multipart/alternative;")
|
||||||
|
fmt.Fprintf(body, "\tboundary=\"%v\"\r\n", wr.Boundary())
|
||||||
|
fmt.Fprintf(body, "\r\n")
|
||||||
|
|
||||||
|
var sub io.Writer
|
||||||
|
|
||||||
|
if len(msg.PlainText) > 0 {
|
||||||
|
sub, err := wr.CreatePart(textproto.MIMEHeader{
|
||||||
|
"Content-Type": []string{"text/plain; charset=UTF-8; format=flowed"},
|
||||||
|
"Content-Transfer-Encoding": []string{"quoted-printable"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := quotedprintable.NewWriter(sub)
|
||||||
|
defer func() { err = errs.Combine(err, enc.Close()) }()
|
||||||
|
|
||||||
|
_, err = enc.Write([]byte(msg.PlainText))
|
||||||
|
if err != nil {
|
||||||
|
return Error.Wrap(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range msg.Parts {
|
||||||
|
header := textproto.MIMEHeader{"Content-Type": []string{mime.QEncoding.Encode("utf-8", part.Type)}}
|
||||||
|
if part.Encoding != "" {
|
||||||
|
header["Content-Transfer-Encoding"] = []string{mime.QEncoding.Encode("utf-8", part.Encoding)}
|
||||||
|
}
|
||||||
|
if part.Disposition != "" {
|
||||||
|
header["Content-Disposition"] = []string{mime.QEncoding.Encode("utf-8", part.Disposition)}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, _ = wr.CreatePart(header)
|
||||||
|
fmt.Fprint(sub, part.Content)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func tocrlf(data []byte) []byte {
|
func tocrlf(data []byte) []byte {
|
||||||
lf := bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
|
lf := bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
|
||||||
crlf := bytes.ReplaceAll(lf, []byte("\n"), []byte("\r\n"))
|
crlf := bytes.ReplaceAll(lf, []byte("\n"), []byte("\r\n"))
|
||||||
|
39
private/post/message_test.go
Normal file
39
private/post/message_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2022 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information
|
||||||
|
|
||||||
|
package post
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/mail"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMessage_ClosingLastPart(t *testing.T) {
|
||||||
|
from := mail.Address{Name: "No reply", Address: "noreply@eu1.storj.io"}
|
||||||
|
|
||||||
|
m := &Message{
|
||||||
|
From: from,
|
||||||
|
To: []mail.Address{{Name: "Foo Bar", Address: "foo@storj.io"}},
|
||||||
|
Subject: "This is a proper test mail",
|
||||||
|
PlainText: "",
|
||||||
|
Parts: []Part{
|
||||||
|
{
|
||||||
|
Type: "text/html; charset=UTF-8",
|
||||||
|
Content: string("<head><body><h1>ahoj</h1></body></head>"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := m.Bytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
// last part should be closed. see 7.2.1 of https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
|
||||||
|
final := regexp.MustCompile("--.+--")
|
||||||
|
lastNonEmptyLine := lines[len(lines)-2]
|
||||||
|
require.True(t, final.MatchString(lastNonEmptyLine), "Last line '%s' doesn't include RFC1341 distinguished delimiter", lastNonEmptyLine)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user