diff --git a/internal/smtp/http.go b/internal/smtp/http.go
index 0adaa8d..68f0adc 100644
--- a/internal/smtp/http.go
+++ b/internal/smtp/http.go
@@ -148,12 +148,14 @@ func (h *smtpHTTPHandler) handleMessageMeta(w http.ResponseWriter, r *http.Reque
return
}
+ content := msg.Content()
+
out := buildMessageBasicMeta(msg)
- out["body_size"] = len(msg.Body())
+ out["body_size"] = len(content.Body())
headers := make(map[string]any)
- for k, v := range msg.Headers() {
+ for k, v := range content.Headers() {
headers[k] = v
}
out["headers"] = headers
@@ -167,12 +169,14 @@ func (h *smtpHTTPHandler) handleMessageBody(w http.ResponseWriter, r *http.Reque
return
}
- contentType, ok := msg.Headers()["Content-Type"]
+ content := msg.Content()
+
+ contentType, ok := content.Headers()["Content-Type"]
if ok {
w.Header()["Content-Type"] = contentType
}
- w.Write(msg.Body())
+ w.Write(content.Body())
}
func (h *smtpHTTPHandler) handleMultipartIndex(w http.ResponseWriter, r *http.Request, idx int) {
@@ -234,20 +238,24 @@ func (h *smtpHTTPHandler) handleMultipartBody(
if msg == nil {
return
}
+
if !ensureIsMultipart(w, msg) {
return
}
+
part := retrievePart(w, msg, partIdx)
if part == nil {
return
}
- contentType, ok := part.Headers()["Content-Type"]
+ content := part.Content()
+
+ contentType, ok := content.Headers()["Content-Type"]
if ok {
w.Header()["Content-Type"] = contentType
}
- w.Write(part.Body())
+ w.Write(content.Body())
}
func (h *smtpHTTPHandler) handleRawMessageData(w http.ResponseWriter, r *http.Request, idx int) {
@@ -291,23 +299,25 @@ func retrievePart(w http.ResponseWriter, msg *ReceivedMessage, partIdx int) *Rec
}
func buildMessageBasicMeta(msg *ReceivedMessage) map[string]any {
+ content := msg.Content()
+
out := map[string]any{
"idx": msg.Index(),
- "isMultipart": msg.IsMultipart(),
+ "isMultipart": content.IsMultipart(),
"receivedAt": msg.ReceivedAt(),
}
- from, ok := msg.Headers()["From"]
+ from, ok := content.Headers()["From"]
if ok {
out["from"] = from
}
- to, ok := msg.Headers()["To"]
+ to, ok := content.Headers()["To"]
if ok {
out["to"] = to
}
- subject, ok := msg.Headers()["Subject"]
+ subject, ok := content.Headers()["Subject"]
if ok && len(subject) == 1 {
out["subject"] = subject[0]
}
@@ -316,13 +326,15 @@ func buildMessageBasicMeta(msg *ReceivedMessage) map[string]any {
}
func buildMultipartMeta(part *ReceivedPart) map[string]any {
+ content := part.Content()
+
out := map[string]any{
"idx": part.Index(),
- "body_size": len(part.Body()),
+ "body_size": len(content.Body()),
}
headers := make(map[string]any)
- for k, v := range part.Headers() {
+ for k, v := range content.Headers() {
headers[k] = v
}
out["headers"] = headers
@@ -334,7 +346,7 @@ func buildMultipartMeta(part *ReceivedPart) map[string]any {
// message, returns true and does nothing further if so, returns false after
// replying with Status 404 if not.
func ensureIsMultipart(w http.ResponseWriter, msg *ReceivedMessage) bool {
- if msg.IsMultipart() {
+ if msg.Content().IsMultipart() {
return true
}
diff --git a/internal/smtp/message.go b/internal/smtp/message.go
index 108bebd..4a6d4a9 100644
--- a/internal/smtp/message.go
+++ b/internal/smtp/message.go
@@ -10,7 +10,6 @@ import (
"mime/multipart"
"mime/quotedprintable"
"net/mail"
- "net/textproto"
"regexp"
"strings"
"time"
@@ -27,14 +26,9 @@ type ReceivedMessage struct {
rawMessageData []byte
receivedAt time.Time
- headers mail.Header
- body []byte
-
- contentType string
- contentTypeParams map[string]string
+ content *ReceivedContent
- isMultipart bool
- multiparts []*ReceivedPart
+ multiparts []*ReceivedPart
}
// ReceivedPart contains a single part of a multipart message as received
@@ -42,8 +36,19 @@ type ReceivedMessage struct {
type ReceivedPart struct {
index int
- headers textproto.MIMEHeader
+ content *ReceivedContent
+}
+
+// ReceivedContent contains the contents of an email message or multipart part.
+type ReceivedContent struct {
+ headers map[string][]string
body []byte
+
+ contentType string
+ contentTypeParams map[string]string
+ isMultipart bool
+
+ // TODO: Move multiparts here from ReceivedMessage in the next step
}
// NewReceivedMessage parses a raw message as received via SMTP into a
@@ -61,16 +66,14 @@ func NewReceivedMessage(
maxMessageSize = DefaultMaxMessageSize
}
- parsedMsg, err := mail.ReadMessage(bytes.NewReader(rawMessageData))
+ parsedMsg, err := mail.ReadMessage(io.LimitReader(bytes.NewReader(rawMessageData), maxMessageSize))
if err != nil {
return nil, fmt.Errorf("could not parse message: %w", err)
}
- preprocessHeaders(parsedMsg.Header)
-
- body, err := io.ReadAll(wrapBodyReader(parsedMsg.Body, parsedMsg.Header, maxMessageSize))
+ content, err := NewReceivedContent(parsedMsg.Header, parsedMsg.Body, maxMessageSize)
if err != nil {
- return nil, fmt.Errorf("could not read message body: %w", err)
+ return nil, fmt.Errorf("could not parse content: %w", err)
}
msg := &ReceivedMessage{
@@ -79,30 +82,16 @@ func NewReceivedMessage(
smtpRcptTo: rcptTo,
rawMessageData: rawMessageData,
receivedAt: receivedAt,
- headers: parsedMsg.Header,
- body: body,
- }
-
- rawContentType := msg.headers.Get("Content-Type")
- if rawContentType != "" {
- msg.contentType, msg.contentTypeParams, err = mime.ParseMediaType(rawContentType)
- if err != nil {
- return nil, fmt.Errorf("could not parse Content-Type: %w", err)
- }
-
- // case-sensitive comparison of the content type is permitted here,
- // since mime.ParseMediaType is documented to return the media type
- // in lower case.
- msg.isMultipart = strings.HasPrefix(msg.contentType, "multipart/")
+ content: content,
}
- if msg.isMultipart {
- boundary, ok := msg.contentTypeParams["boundary"]
+ if content.IsMultipart() {
+ boundary, ok := content.ContentTypeParams()["boundary"]
if !ok {
return nil, fmt.Errorf("encountered multipart message without defined boundary")
}
- r := multipart.NewReader(bytes.NewReader(msg.body), boundary)
+ r := multipart.NewReader(bytes.NewReader(content.body), boundary)
for i := 0; ; i++ {
rawPart, err := r.NextRawPart()
@@ -136,7 +125,9 @@ func NewReceivedMessage(
// If no matching multiparts are found, this may return nil or an empty
// list.
func (m *ReceivedMessage) SearchPartsByHeader(re *regexp.Regexp) []*ReceivedPart {
- if !m.IsMultipart() {
+ // TODO: Somehow unify with Server.SearchByHeader based on ReceivedContent
+
+ if !m.content.IsMultipart() {
return nil
}
@@ -144,7 +135,7 @@ func (m *ReceivedMessage) SearchPartsByHeader(re *regexp.Regexp) []*ReceivedPart
headerIdxList := make([]map[string][]string, len(multiparts))
for i, v := range multiparts {
- headerIdxList[i] = v.Headers()
+ headerIdxList[i] = v.Content().Headers()
}
foundIndices := searchByHeaderCommon(headerIdxList, re)
@@ -159,45 +150,81 @@ func (m *ReceivedMessage) SearchPartsByHeader(re *regexp.Regexp) []*ReceivedPart
// NewReceivedPart parses a MIME multipart part into a ReceivedPart struct.
//
+// maxMessageSize is passed through to NewReceivedContent (see its documentation for details).
+func NewReceivedPart(index int, p *multipart.Part, maxMessageSize int64) (*ReceivedPart, error) {
+ content, err := NewReceivedContent(p.Header, p, maxMessageSize)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse content: %w", err)
+ }
+
+ part := &ReceivedPart{
+ index: index,
+ content: content,
+ }
+
+ return part, nil
+}
+
+// NewReceivedContent parses a message or part headers and body into a ReceivedContent struct.
+//
// Incoming data is truncated after the given maximum message size.
// If a maxMessageSize of 0 is given, this function will default to using
// DefaultMaxMessageSize.
-func NewReceivedPart(index int, p *multipart.Part, maxMessageSize int64) (*ReceivedPart, error) {
+func NewReceivedContent(
+ headers map[string][]string, bodyReader io.Reader, maxMessageSize int64,
+) (*ReceivedContent, error) {
if maxMessageSize == 0 {
maxMessageSize = DefaultMaxMessageSize
}
- preprocessHeaders(p.Header)
+ headers = preprocessHeaders(headers)
- body, err := io.ReadAll(wrapBodyReader(p, p.Header, maxMessageSize))
+ body, err := io.ReadAll(wrapBodyReader(bodyReader, headers, maxMessageSize))
if err != nil {
- return nil, fmt.Errorf("could not read message part body: %w", err)
+ return nil, fmt.Errorf("could not read body: %w", err)
}
- part := &ReceivedPart{
- index: index,
- headers: p.Header,
+ content := &ReceivedContent{
+ headers: headers,
body: body,
}
- return part, nil
+ rawContentType, ok := headers["Content-Type"]
+ if ok && rawContentType[0] != "" && len(rawContentType) > 0 {
+ content.contentType, content.contentTypeParams, err = mime.ParseMediaType(rawContentType[0])
+ if err != nil {
+ return nil, fmt.Errorf("could not parse Content-Type: %w", err)
+ }
+
+ // case-sensitive comparison of the content type is permitted here,
+ // since mime.ParseMediaType is documented to return the media type
+ // in lower case.
+ content.isMultipart = strings.HasPrefix(content.contentType, "multipart/")
+ }
+
+ return content, nil
}
-// preprocessHeaders modifies the given headers in-place by decoding
-// header values that were encoded according to RFC2047.
-func preprocessHeaders(headers map[string][]string) {
+// preprocessHeaders decodes header values that were encoded according to RFC2047.
+func preprocessHeaders(headers map[string][]string) map[string][]string {
var decoder mime.WordDecoder
- for _, vs := range headers {
+ out := make(map[string][]string)
+
+ for k, vs := range headers {
+ out[k] = make([]string, len(vs))
+
for i := range vs {
dec, err := decoder.DecodeHeader(vs[i])
if err != nil {
logrus.Warn("could not decode Q-Encoding in header:", err)
} else {
- vs[i] = dec
+ out[k][i] = dec
}
}
}
+
+ return out
}
// wrapBodyReader wraps the reader for a message / part body with size
@@ -229,24 +256,32 @@ func wrapBodyReader(r io.Reader, headers map[string][]string, maxMessageSize int
// Getters
// =======
-func (m *ReceivedMessage) ContentType() string {
- return m.contentType
+func (c *ReceivedContent) ContentType() string {
+ return c.contentType
}
-func (m *ReceivedMessage) Body() []byte {
- return m.body
+func (c *ReceivedContent) ContentTypeParams() map[string]string {
+ return c.contentTypeParams
}
-func (m *ReceivedMessage) Headers() mail.Header {
- return m.headers
+func (c *ReceivedContent) Body() []byte {
+ return c.body
}
-func (m *ReceivedMessage) Index() int {
- return m.index
+func (c *ReceivedContent) Headers() map[string][]string {
+ return c.headers
+}
+
+func (c *ReceivedContent) IsMultipart() bool {
+ return c.isMultipart
}
-func (m *ReceivedMessage) IsMultipart() bool {
- return m.isMultipart
+func (m *ReceivedMessage) Content() *ReceivedContent {
+ return m.content
+}
+
+func (m *ReceivedMessage) Index() int {
+ return m.index
}
func (m *ReceivedMessage) Multiparts() []*ReceivedPart {
@@ -269,12 +304,8 @@ func (m *ReceivedMessage) SmtpRcptTo() []string {
return m.smtpRcptTo
}
-func (p *ReceivedPart) Body() []byte {
- return p.body
-}
-
-func (p *ReceivedPart) Headers() textproto.MIMEHeader {
- return p.headers
+func (p *ReceivedPart) Content() *ReceivedContent {
+ return p.content
}
func (p *ReceivedPart) Index() int {
diff --git a/internal/smtp/server.go b/internal/smtp/server.go
index 4f13e83..eeb3791 100644
--- a/internal/smtp/server.go
+++ b/internal/smtp/server.go
@@ -119,6 +119,8 @@ func (s *Server) ReceivedMessages() []*ReceivedMessage {
// being encoded as if for transport (e.g. quoted-printable),
// but concatenated as-is.
func (s *Server) SearchByHeader(re *regexp.Regexp) []*ReceivedMessage {
+ // TODO: Somehow unify with ReceivedMessage.SearchPartsByHeader based on ReceivedContent
+
s.mutex.RLock()
defer s.mutex.RUnlock()
@@ -126,7 +128,7 @@ func (s *Server) SearchByHeader(re *regexp.Regexp) []*ReceivedMessage {
headerIdxList := make([]map[string][]string, len(receivedMessages))
for i, v := range receivedMessages {
- headerIdxList[i] = v.Headers()
+ headerIdxList[i] = v.Content().Headers()
}
foundIndices := searchByHeaderCommon(headerIdxList, re)
diff --git a/internal/smtp/smtp_test.go b/internal/smtp/smtp_test.go
index 0cb0620..8f20938 100644
--- a/internal/smtp/smtp_test.go
+++ b/internal/smtp/smtp_test.go
@@ -186,12 +186,8 @@ func assertMessageEqual(t *testing.T, expected, actual *ReceivedMessage) {
assert.ElementsMatch(t, expected.smtpRcptTo, actual.smtpRcptTo)
assert.Equal(t, expected.rawMessageData, actual.rawMessageData)
assert.Equal(t, expected.receivedAt, actual.receivedAt)
- assert.Equal(t, expected.body, actual.body)
- assert.Equal(t, expected.contentType, actual.contentType)
- assert.Equal(t, expected.contentTypeParams, actual.contentTypeParams)
- assert.Equal(t, expected.isMultipart, actual.isMultipart)
- assertHeadersEqual(t, expected.headers, actual.headers)
+ assertContentEqual(t, expected.content, actual.content)
if assert.Equal(t, len(expected.multiparts), len(actual.multiparts)) {
for i, m := range expected.multiparts {
@@ -202,8 +198,16 @@ func assertMessageEqual(t *testing.T, expected, actual *ReceivedMessage) {
func assertMultipartEqual(t *testing.T, expected, actual *ReceivedPart) {
assert.Equal(t, expected.index, actual.index)
- assertHeadersEqual(t, expected.headers, actual.headers)
+ assertContentEqual(t, expected.content, actual.content)
+}
+
+func assertContentEqual(t *testing.T, expected, actual *ReceivedContent) {
assert.Equal(t, expected.body, actual.body)
+ assert.Equal(t, expected.contentType, actual.contentType)
+ assert.Equal(t, expected.contentTypeParams, actual.contentTypeParams)
+ assert.Equal(t, expected.isMultipart, actual.isMultipart)
+
+ assertHeadersEqual(t, expected.headers, actual.headers)
}
// runTestSession starts a Server, runs a pre-recorded SMTP session,
@@ -250,12 +254,14 @@ To: testreceiver@programmfabrik.de
Hello World!
A simple plain text test mail.`),
receivedAt: testTime,
- headers: map[string][]string{
- "From": {"testsender@programmfabrik.de"},
- "To": {"testreceiver@programmfabrik.de"},
- },
- body: []byte(`Hello World!
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "From": {"testsender@programmfabrik.de"},
+ "To": {"testreceiver@programmfabrik.de"},
+ },
+ body: []byte(`Hello World!
A simple plain text test mail.`),
+ },
},
{
index: 1,
@@ -282,15 +288,16 @@ Some text in HTML format.
Trailing text is ignored.`),
receivedAt: testTime,
- headers: map[string][]string{
- "Mime-Version": {"1.0"},
- "From": {"testsender2@programmfabrik.de"},
- "To": {"testreceiver2@programmfabrik.de"},
- "Date": {"Tue, 25 Jun 2024 11:15:57 +0200"},
- "Subject": {"Example Message"},
- "Content-Type": {`multipart/mixed; boundary="d36c3118be4745f9a1cb4556d11fe92d"`},
- },
- body: []byte(`Preamble is ignored.
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Mime-Version": {"1.0"},
+ "From": {"testsender2@programmfabrik.de"},
+ "To": {"testreceiver2@programmfabrik.de"},
+ "Date": {"Tue, 25 Jun 2024 11:15:57 +0200"},
+ "Subject": {"Example Message"},
+ "Content-Type": {`multipart/mixed; boundary="d36c3118be4745f9a1cb4556d11fe92d"`},
+ },
+ body: []byte(`Preamble is ignored.
--d36c3118be4745f9a1cb4556d11fe92d
Content-type: text/plain; charset=utf-8
@@ -303,25 +310,38 @@ Some text in HTML format.
--d36c3118be4745f9a1cb4556d11fe92d--
Trailing text is ignored.`),
- contentType: "multipart/mixed",
- contentTypeParams: map[string]string{
- "boundary": "d36c3118be4745f9a1cb4556d11fe92d",
+ contentType: "multipart/mixed",
+ contentTypeParams: map[string]string{
+ "boundary": "d36c3118be4745f9a1cb4556d11fe92d",
+ },
+ isMultipart: true,
},
- isMultipart: true,
multiparts: []*ReceivedPart{
{
index: 0,
- headers: map[string][]string{
- "Content-Type": {"text/plain; charset=utf-8"},
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Content-Type": {"text/plain; charset=utf-8"},
+ },
+ body: []byte(`Some plain text`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
- body: []byte(`Some plain text`),
},
{
index: 1,
- headers: map[string][]string{
- "Content-Type": {"text/html; charset=utf-8"},
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Content-Type": {"text/html; charset=utf-8"},
+ },
+ body: []byte(`Some text in HTML format.`),
+ contentType: "text/html",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
- body: []byte(`Some text in HTML format.`),
},
},
},
@@ -335,15 +355,17 @@ Content-Type: text/plain; charset=utf-8
Noch eine Testmail. Diesmal mit nicht-ASCII-Zeichen: äöüß`),
receivedAt: testTime,
- headers: map[string][]string{
- "From": {"testsender3@programmfabrik.de"},
- "To": {"testreceiver3@programmfabrik.de"},
- "Content-Type": {"text/plain; charset=utf-8"},
- },
- body: []byte(`Noch eine Testmail. Diesmal mit nicht-ASCII-Zeichen: äöüß`),
- contentType: "text/plain",
- contentTypeParams: map[string]string{
- "charset": "utf-8",
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "From": {"testsender3@programmfabrik.de"},
+ "To": {"testreceiver3@programmfabrik.de"},
+ "Content-Type": {"text/plain; charset=utf-8"},
+ },
+ body: []byte(`Noch eine Testmail. Diesmal mit nicht-ASCII-Zeichen: äöüß`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
},
{
@@ -358,17 +380,19 @@ Content-Transfer-Encoding: base64
RWluZSBiYXNlNjQtZW5rb2RpZXJ0ZSBUZXN0bWFpbCBtaXQgbmljaHQtQVNDSUktWmVpY2hlbjog
w6TDtsO8w58K`),
receivedAt: testTime,
- headers: map[string][]string{
- "From": {"testsender4@programmfabrik.de"},
- "To": {"testreceiver4@programmfabrik.de"},
- "Content-Type": {"text/plain; charset=utf-8"},
- "Content-Transfer-Encoding": {"base64"},
- },
- body: []byte(`Eine base64-enkodierte Testmail mit nicht-ASCII-Zeichen: äöüß
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "From": {"testsender4@programmfabrik.de"},
+ "To": {"testreceiver4@programmfabrik.de"},
+ "Content-Type": {"text/plain; charset=utf-8"},
+ "Content-Transfer-Encoding": {"base64"},
+ },
+ body: []byte(`Eine base64-enkodierte Testmail mit nicht-ASCII-Zeichen: äöüß
`),
- contentType: "text/plain",
- contentTypeParams: map[string]string{
- "charset": "utf-8",
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
},
{
@@ -383,16 +407,18 @@ Content-Transfer-Encoding: quoted-printable
Noch eine Testmail mit =C3=A4=C3=B6=C3=BC=C3=9F, diesmal enkodiert in quote=
d-printable.`),
receivedAt: testTime,
- headers: map[string][]string{
- "From": {"testsender5@programmfabrik.de"},
- "To": {"testreceiver5@programmfabrik.de"},
- "Content-Type": {"text/plain; charset=utf-8"},
- "Content-Transfer-Encoding": {"quoted-printable"},
- },
- body: []byte(`Noch eine Testmail mit äöüß, diesmal enkodiert in quoted-printable.`),
- contentType: "text/plain",
- contentTypeParams: map[string]string{
- "charset": "utf-8",
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "From": {"testsender5@programmfabrik.de"},
+ "To": {"testreceiver5@programmfabrik.de"},
+ "Content-Type": {"text/plain; charset=utf-8"},
+ "Content-Transfer-Encoding": {"quoted-printable"},
+ },
+ body: []byte(`Noch eine Testmail mit äöüß, diesmal enkodiert in quoted-printable.`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
},
{
@@ -420,15 +446,16 @@ Noch eine Testmail mit =C3=A4=C3=B6=C3=BC=C3=9F, diesmal enkodiert in quote=
d-printable.
--d36c3118be4745f9a1cb4556d11fe92d--`),
receivedAt: testTime,
- headers: map[string][]string{
- "Mime-Version": {"1.0"},
- "From": {"testsender6@programmfabrik.de"},
- "To": {"testreceiver6@programmfabrik.de"},
- "Date": {"Tue, 25 Jun 2024 11:15:57 +0200"},
- "Subject": {"Example Message"},
- "Content-Type": {`multipart/mixed; boundary="d36c3118be4745f9a1cb4556d11fe92d"`},
- },
- body: []byte(`--d36c3118be4745f9a1cb4556d11fe92d
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Mime-Version": {"1.0"},
+ "From": {"testsender6@programmfabrik.de"},
+ "To": {"testreceiver6@programmfabrik.de"},
+ "Date": {"Tue, 25 Jun 2024 11:15:57 +0200"},
+ "Subject": {"Example Message"},
+ "Content-Type": {`multipart/mixed; boundary="d36c3118be4745f9a1cb4556d11fe92d"`},
+ },
+ body: []byte(`--d36c3118be4745f9a1cb4556d11fe92d
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
@@ -441,28 +468,41 @@ Content-Transfer-Encoding: quoted-printable
Noch eine Testmail mit =C3=A4=C3=B6=C3=BC=C3=9F, diesmal enkodiert in quote=
d-printable.
--d36c3118be4745f9a1cb4556d11fe92d--`),
- contentType: "multipart/mixed",
- contentTypeParams: map[string]string{
- "boundary": "d36c3118be4745f9a1cb4556d11fe92d",
+ contentType: "multipart/mixed",
+ contentTypeParams: map[string]string{
+ "boundary": "d36c3118be4745f9a1cb4556d11fe92d",
+ },
+ isMultipart: true,
},
- isMultipart: true,
multiparts: []*ReceivedPart{
{
index: 0,
- headers: map[string][]string{
- "Content-Type": {"text/plain; charset=utf-8"},
- "Content-Transfer-Encoding": {"base64"},
- },
- body: []byte(`Eine base64-enkodierte Testmail mit nicht-ASCII-Zeichen: äöüß
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Content-Type": {"text/plain; charset=utf-8"},
+ "Content-Transfer-Encoding": {"base64"},
+ },
+ body: []byte(`Eine base64-enkodierte Testmail mit nicht-ASCII-Zeichen: äöüß
`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
+ },
},
{
index: 1,
- headers: map[string][]string{
- "Content-Type": {"text/plain; charset=utf-8"},
- "Content-Transfer-Encoding": {"quoted-printable"},
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Content-Type": {"text/plain; charset=utf-8"},
+ "Content-Transfer-Encoding": {"quoted-printable"},
+ },
+ body: []byte(`Noch eine Testmail mit äöüß, diesmal enkodiert in quoted-printable.`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
- body: []byte(`Noch eine Testmail mit äöüß, diesmal enkodiert in quoted-printable.`),
},
},
},
@@ -477,13 +517,15 @@ Subject: Tästmail mit Ümlauten im Header
Hello World!
A simple plain text test mail.`),
receivedAt: testTime,
- headers: map[string][]string{
- "From": {"tästsender7@programmfabrik.de"},
- "To": {"testreceiver7@programmfabrik.de"},
- "Subject": {"Tästmail mit Ümlauten im Header"},
- },
- body: []byte(`Hello World!
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "From": {"tästsender7@programmfabrik.de"},
+ "To": {"testreceiver7@programmfabrik.de"},
+ "Subject": {"Tästmail mit Ümlauten im Header"},
+ },
+ body: []byte(`Hello World!
A simple plain text test mail.`),
+ },
},
{
index: 7,
@@ -496,13 +538,15 @@ Subject: =?utf-8?q?T=C3=A4stmail_mit_=C3=9Cmlauten_im_Header?=
Hello World!
A simple plain text test mail.`),
receivedAt: testTime,
- headers: map[string][]string{
- "From": {"tästsender8@programmfabrik.de"},
- "To": {"testreceiver8@programmfabrik.de"},
- "Subject": {"Tästmail mit Ümlauten im Header"},
- },
- body: []byte(`Hello World!
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "From": {"tästsender8@programmfabrik.de"},
+ "To": {"testreceiver8@programmfabrik.de"},
+ "Subject": {"Tästmail mit Ümlauten im Header"},
+ },
+ body: []byte(`Hello World!
A simple plain text test mail.`),
+ },
},
{
index: 8,
@@ -531,15 +575,16 @@ Noch eine Testmail mit =C3=A4=C3=B6=C3=BC=C3=9F, diesmal enkodiert in quote=
d-printable.
--d36c3118be4745f9a1cb4556d11fe92d--`),
receivedAt: testTime,
- headers: map[string][]string{
- "Mime-Version": {"1.0"},
- "From": {"testsender9@programmfabrik.de"},
- "To": {"testreceiver9@programmfabrik.de"},
- "Date": {"Tue, 25 Jun 2024 11:15:57 +0200"},
- "Subject": {"Example Message"},
- "Content-Type": {`multipart/mixed; boundary="d36c3118be4745f9a1cb4556d11fe92d"`},
- },
- body: []byte(`--d36c3118be4745f9a1cb4556d11fe92d
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Mime-Version": {"1.0"},
+ "From": {"testsender9@programmfabrik.de"},
+ "To": {"testreceiver9@programmfabrik.de"},
+ "Date": {"Tue, 25 Jun 2024 11:15:57 +0200"},
+ "Subject": {"Example Message"},
+ "Content-Type": {`multipart/mixed; boundary="d36c3118be4745f9a1cb4556d11fe92d"`},
+ },
+ body: []byte(`--d36c3118be4745f9a1cb4556d11fe92d
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
X-Funky-Header: =?utf-8?q?T=C3=A4stmail_mit_=C3=9Cmlauten_im_Header?=
@@ -554,30 +599,43 @@ X-Funky-Header: Käse
Noch eine Testmail mit =C3=A4=C3=B6=C3=BC=C3=9F, diesmal enkodiert in quote=
d-printable.
--d36c3118be4745f9a1cb4556d11fe92d--`),
- contentType: "multipart/mixed",
- contentTypeParams: map[string]string{
- "boundary": "d36c3118be4745f9a1cb4556d11fe92d",
+ contentType: "multipart/mixed",
+ contentTypeParams: map[string]string{
+ "boundary": "d36c3118be4745f9a1cb4556d11fe92d",
+ },
+ isMultipart: true,
},
- isMultipart: true,
multiparts: []*ReceivedPart{
{
index: 0,
- headers: map[string][]string{
- "Content-Type": {"text/plain; charset=utf-8"},
- "Content-Transfer-Encoding": {"base64"},
- "X-Funky-Header": {"Tästmail mit Ümlauten im Header"},
- },
- body: []byte(`Eine base64-enkodierte Testmail mit nicht-ASCII-Zeichen: äöüß
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Content-Type": {"text/plain; charset=utf-8"},
+ "Content-Transfer-Encoding": {"base64"},
+ "X-Funky-Header": {"Tästmail mit Ümlauten im Header"},
+ },
+ body: []byte(`Eine base64-enkodierte Testmail mit nicht-ASCII-Zeichen: äöüß
`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
+ },
},
{
index: 1,
- headers: map[string][]string{
- "Content-Type": {"text/plain; charset=utf-8"},
- "Content-Transfer-Encoding": {"quoted-printable"},
- "X-Funky-Header": {"Käse"},
+ content: &ReceivedContent{
+ headers: map[string][]string{
+ "Content-Type": {"text/plain; charset=utf-8"},
+ "Content-Transfer-Encoding": {"quoted-printable"},
+ "X-Funky-Header": {"Käse"},
+ },
+ body: []byte(`Noch eine Testmail mit äöüß, diesmal enkodiert in quoted-printable.`),
+ contentType: "text/plain",
+ contentTypeParams: map[string]string{
+ "charset": "utf-8",
+ },
},
- body: []byte(`Noch eine Testmail mit äöüß, diesmal enkodiert in quoted-printable.`),
},
},
},
@@ -596,17 +654,17 @@ d-printable.
m.rawMessageData = appendCRLF(m.rawMessageData)
// Format message body only if not in base64 transfer encoding
- cte, ok := m.headers["Content-Transfer-Encoding"]
+ cte, ok := m.content.headers["Content-Transfer-Encoding"]
if !ok || len(cte) != 1 || cte[0] != "base64" {
- m.body = formatRaw(m.body)
- m.body = appendCRLF(m.body)
+ m.content.body = formatRaw(m.content.body)
+ m.content.body = appendCRLF(m.content.body)
}
for _, p := range m.multiparts {
// Format multipart body only if not in base64 transfer encoding
- cte, ok = p.headers["Content-Transfer-Encoding"]
+ cte, ok = p.content.headers["Content-Transfer-Encoding"]
if !ok || len(cte) != 1 || cte[0] != "base64" {
- p.body = formatRaw(p.body)
+ p.content.body = formatRaw(p.content.body)
}
// Multiparts do not add a trailing CRLF