1
1
Fork 0
mirror of https://github.com/mjl-/mox.git synced 2025-04-21 21:40:01 +03:00

write base64 message parts with 76 data bytes on a line instead of 78

As required by RFC 2045 (MIME). The 78 byte lines work in practice, except that
SpamAssassin has rules that give messages with 78-byte lines spam points.

Mentioned by kjetilho on irc.
This commit is contained in:
Mechiel Lukkien 2025-04-03 10:22:15 +02:00
parent 00c8db98e6
commit 69d2699961
No known key found for this signature in database
9 changed files with 16 additions and 13 deletions

View file

@ -340,7 +340,7 @@ func (m *Message) Compose(log mlog.Log, smtputf8 bool) ([]byte, error) {
data := base64.StdEncoding.EncodeToString(headers)
for len(data) > 0 {
line := data
n := min(len(line), 78)
n := min(len(line), 76) // ../rfc/2045:1372
line, data = data[:n], data[n:]
if _, err := origp.Write([]byte(line + "\r\n")); err != nil {
return nil, err

View file

@ -64,7 +64,7 @@ func TestCompressBreak(t *testing.T) {
tcheck(t, err, "read random")
text := base64.StdEncoding.EncodeToString(buf)
for len(text) > 0 {
n := min(78, len(text))
n := min(76, len(text))
msg += text[:n] + "\r\n"
text = text[n:]
}

View file

@ -27,7 +27,7 @@ func (w *HeaderWriter) Add(separator string, texts ...string) {
}
for _, text := range texts {
n := len(text)
if w.nonfirst && w.lineLen > 1 && w.lineLen+len(separator)+n > 78 {
if w.nonfirst && w.lineLen > 1 && w.lineLen+len(separator)+n > 76 {
w.b.WriteString("\r\n\t")
w.lineLen = 1
} else if w.nonfirst && separator != "" {
@ -45,7 +45,7 @@ func (w *HeaderWriter) Add(separator string, texts ...string) {
func (w *HeaderWriter) AddWrap(buf []byte, text bool) {
for len(buf) > 0 {
line := buf
n := 78 - w.lineLen
n := 76 - w.lineLen
if len(buf) > n {
if text {
if i := bytes.LastIndexAny(buf[:n], " \t"); i > 0 {

View file

@ -11,7 +11,9 @@ import (
func NeedsQuotedPrintable(text string) bool {
// ../rfc/2045:1025
for _, line := range strings.Split(text, "\r\n") {
if len(line) > 78 || strings.Contains(line, "\r") || strings.Contains(line, "\n") {
// 78 should be fine too, qp itself has a requirement of 76 bytes on a line, but
// using qp for anything longer than 76 is safer.
if len(line) > 76 || strings.Contains(line, "\r") || strings.Contains(line, "\n") {
return true
}
}

View file

@ -13,7 +13,7 @@ func (f closerFunc) Close() error {
}
// Base64Writer turns a writer for data into one that writes base64 content on
// \r\n separated lines of max 78+2 characters length.
// \r\n separated lines of max 76+2 characters length.
func Base64Writer(w io.Writer) io.WriteCloser {
lw := &lineWrapper{w: w}
bw := base64.NewEncoder(base64.StdEncoding, lw)
@ -39,7 +39,8 @@ type lineWrapper struct {
func (lw *lineWrapper) Write(buf []byte) (int, error) {
wrote := 0
for len(buf) > 0 {
n := min(78-lw.n, len(buf))
// base64 has max 76 data bytes on per line. ../rfc/2045:1372
n := min(76-lw.n, len(buf))
nn, err := lw.w.Write(buf[:n])
if nn > 0 {
wrote += nn
@ -49,7 +50,7 @@ func (lw *lineWrapper) Write(buf []byte) (int, error) {
return wrote, err
}
lw.n += nn
if lw.n == 78 {
if lw.n == 76 {
_, err := lw.w.Write([]byte("\r\n"))
if err != nil {
return wrote, err

View file

@ -13,7 +13,7 @@ func TestBase64Writer(t *testing.T) {
err = bw.Close()
tcheckf(t, err, "close")
s := sb.String()
exp := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nz\r\ng5MDEyMzQ1Njc4OQ==\r\n"
exp := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2\r\nNzg5MDEyMzQ1Njc4OQ==\r\n"
if s != exp {
t.Fatalf("base64writer, got %q, expected %q", s, exp)
}

View file

@ -26,7 +26,7 @@ func TestReceived(t *testing.T) {
Receiver: "z",
Identity: ReceivedMailFrom,
Mechanism: "+ip4:0.0.0.0/0",
}, "Received-SPF: pass (c) client-ip=0.0.0.0; envelope-from=\"x@x\"; helo=y;\r\n\tproblem=\"a b\\\"\\\\\"; mechanism=\"+ip4:0.0.0.0/0\"; receiver=z; identity=mailfrom\r\n")
}, "Received-SPF: pass (c) client-ip=0.0.0.0; envelope-from=\"x@x\"; helo=y;\r\n\tproblem=\"a b\\\"\\\\\"; mechanism=\"+ip4:0.0.0.0/0\"; receiver=z;\r\n\tidentity=mailfrom\r\n")
test(Received{
Result: StatusPass,
@ -35,5 +35,5 @@ func TestReceived(t *testing.T) {
Helo: dns.IPDomain{IP: net.ParseIP("2001:db8::1")},
Receiver: "z",
Identity: ReceivedMailFrom,
}, "Received-SPF: pass client-ip=0.0.0.0; envelope-from=\"x@x\"; helo=\"2001:db8::1\";\r\n\treceiver=z; identity=mailfrom\r\n")
}, "Received-SPF: pass client-ip=0.0.0.0; envelope-from=\"x@x\";\r\n\thelo=\"2001:db8::1\"; receiver=z; identity=mailfrom\r\n")
}

View file

@ -875,7 +875,7 @@ func (s server) Send(ctx context.Context, req webapi.SendRequest) (resp webapi.S
for len(base64Data) > 0 {
line := base64Data
n := min(len(line), 78)
n := min(len(line), 76) // ../rfc/2045:1372
line, base64Data = base64Data[:n], base64Data[n:]
_, err := p.Write([]byte(line))
xcheckf(err, "writing attachment")

View file

@ -831,7 +831,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
for len(base64Data) > 0 {
line := base64Data
n := min(len(line), 78)
n := min(len(line), 76) // ../rfc/2045:1372
line, base64Data = base64Data[:n], base64Data[n:]
_, err := ap.Write(line)
xcheckf(ctx, err, "writing attachment")