From 361bc2b516d5847005a8f79b518a6f5e7fa430fc Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Tue, 21 Nov 2023 13:19:54 +0100 Subject: [PATCH] when accepting an incoming message, turn any bare newlines (without carriage return) into crlf because that is what most of the code expects. we could work around having bare lf, but it would complicate too much code. currently, a message with bare lf is accepted (in smtpserver delivery, imapserver append, etc), but when an imap session would try to fetch parsed parts, that would fail because and even cause a imapserver panic (closing the connection). in message imports we would already convert bare lf to crlf (because it is expected those messages are all lf-only-ending). we store messages with crlf-ending instead of lf-ending so the imapserver has all correct information at hand (line counts, byte counts). found by using emclient with mox. it adds a message to the inbox that can have mixed crlf and bare lf line endings in a few header fields (in some localization, emclient authors explained how that happened, thanks!). we can now convert those lines and read those messages over imap. emclient already switched to all-crlf line endings in newer (development) versions. --- message/part.go | 1 - message/writer.go | 50 +++++++++++++++++++++++++++++++++++++----- message/writer_test.go | 15 ++++++++++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/message/part.go b/message/part.go index 7c6c949..fd2e9e8 100644 --- a/message/part.go +++ b/message/part.go @@ -490,7 +490,6 @@ func parseAddressList(log *mlog.Log, h mail.Header, k string) []Address { var user, host string addr, err := smtp.ParseAddress(a.Address) if err != nil { - // todo: pass a ctx to this function so we can log with cid. log.Infox("parsing address (continuing)", err, mlog.Field("address", a.Address)) } else { user = addr.Localpart.String() diff --git a/message/writer.go b/message/writer.go index 79e1920..4b7bc92 100644 --- a/message/writer.go +++ b/message/writer.go @@ -5,7 +5,7 @@ import ( ) // Writer is a write-through helper, collecting properties about the written -// message. +// message and replacing bare \n line endings with \r\n. type Writer struct { writer io.Writer @@ -24,6 +24,8 @@ func NewWriter(w io.Writer) *Writer { // Write implements io.Writer. func (w *Writer) Write(buf []byte) (int, error) { + origtail := w.tail + if !w.HaveBody && len(buf) > 0 { get := func(i int) byte { if i < 0 { @@ -33,7 +35,7 @@ func (w *Writer) Write(buf []byte) (int, error) { } for i, b := range buf { - if b == '\n' && get(i-3) == '\r' && get(i-2) == '\n' && get(i-1) == '\r' { + if b == '\n' && (get(i-1) == '\n' || get(i-1) == '\r' && get(i-2) == '\n') { w.HaveBody = true break } @@ -54,9 +56,45 @@ func (w *Writer) Write(buf []byte) (int, error) { } } } - n, err := w.writer.Write(buf) - if n > 0 { - w.Size += int64(n) + + wrote := 0 + o := 0 +Top: + for o < len(buf) { + for i := o; i < len(buf); i++ { + if buf[i] == '\n' && (i > 0 && buf[i-1] != '\r' || i == 0 && origtail[2] != '\r') { + // Write buffer leading up to missing \r. + if i > o+1 { + n, err := w.writer.Write(buf[o:i]) + if n > 0 { + wrote += n + w.Size += int64(n) + } + if err != nil { + return wrote, err + } + } + n, err := w.writer.Write([]byte{'\r', '\n'}) + if n == 2 { + wrote += 1 // For only the newline. + w.Size += int64(2) + } + if err != nil { + return wrote, err + } + o = i + 1 + continue Top + } + } + n, err := w.writer.Write(buf[o:]) + if n > 0 { + wrote += n + w.Size += int64(n) + } + if err != nil { + return wrote, err + } + break } - return n, err + return wrote, nil } diff --git a/message/writer_test.go b/message/writer_test.go index 49b9991..65f1cae 100644 --- a/message/writer_test.go +++ b/message/writer_test.go @@ -34,9 +34,22 @@ func TestMsgWriter(t *testing.T) { check("no header\r\n", false) check("key: value\r\n\r\n", true) check("key: value\r\n\r\nbody", true) - check("key: value\n\nbody", false) + check("key: value\n\nbody", true) + check("key: value\n\r\nbody", true) check("key: value\r\rbody", false) check("\r\n\r\n", true) check("\r\n\r\nbody", true) check("\r\nbody", true) + + // Check \n is replaced with \r\n. + var b strings.Builder + mw := NewWriter(&b) + msg := "key: value\n\nline1\r\nline2\n" + _, err := mw.Write([]byte(msg)) + tcheck(t, err, "write") + got := b.String() + exp := "key: value\r\n\r\nline1\r\nline2\r\n" + if got != exp { + t.Fatalf("got %q, expected %q", got, exp) + } }