golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.28k stars 17.47k forks source link

mime/quotedprintable: single line with only period cause message truncation when used in SMTP DATA #61235

Open shuLhan opened 1 year ago

shuLhan commented 1 year ago

What version of Go are you using (go version)?

$ go version
go version devel go1.21-a618094c2a Sat Jul 8 13:45:19 2023 +0700 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE='on'
GOARCH='amd64'
GOBIN='/home/ms/go/bin'
GOCACHE='/home/ms/.cache/go-build'
GOENV='/home/ms/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/ms/go/pkg/mod'
GONOPROXY='git.sr.ht'
GONOSUMDB='git.sr.ht'
GOOS='linux'
GOPATH='/home/ms/go'
GOPRIVATE='git.sr.ht'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/ms/opt/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/ms/opt/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='devel go1.21-a618094c2a Sat Jul 8 13:45:19 2023 +0700'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='0'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build721925861=/tmp/go-build -gno-record-gcc-switches'

What did you do?

Given the following example of message body,

A line that precisely have length 75 with . + LF will cause DATA truncation.\n
\n
Footer.\n

The quotedprintable Writer will encode the message into,

A line that precisely have length 75 with . + LF will cause DATA truncation=\r\n
.\r\n
\r\n
Footer.\r\n

If we pass the Writer output into SMTP DATA command, the server read the "\r\n.\r\n" as the end of DATA which cause the message truncated on the receiver.

Reproduceable code

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/shuLhan/share/lib/email"
    "github.com/shuLhan/share/lib/smtp"
)

func main() {
    // Set up authentication information.
    var (
        smtpUser       = os.Getenv(`SMTP_USER`)
        smtpPass       = os.Getenv(`SMTP_PASS`)
        smtpServer     = os.Getenv(`SMTP_SERVER`)
        smtpServerPort = os.Getenv(`SMTP_SERVER_PORT`)
        from           = os.Getenv(`SMTP_FROM`)
        to             = os.Getenv(`SMTP_TO`)
    )

    fmt.Printf("SMTP user: %s\n", smtpUser)
    fmt.Printf("SMTP pass: %s\n", smtpPass)
    fmt.Printf("SMTP addr: %s:%s\n", smtpServer, smtpServerPort)
    fmt.Printf("From     : %s\n", from)
    fmt.Printf("To       : %s\n", to)

    var (
        subject  = `Truncated message`
        bodyText = "A line that precisely have length 75 with . + LF will cause DATA truncation.\n" +
            "\n" +
            "This line will not be received.\n"

        msg    email.Message
        mailtx *smtp.MailTx
        data   []byte
        err    error
    )

    err = msg.SetFrom(from)
    if err != nil {
        log.Fatal(err)
    }
    err = msg.SetTo(to)
    if err != nil {
        log.Fatal(err)
    }
    msg.SetSubject(subject)
    err = msg.SetBodyText([]byte(bodyText))
    if err != nil {
        log.Fatal(err)
    }
    data, err = msg.Pack()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Data: %s\n", data)

    mailtx = smtp.NewMailTx(from, []string{to}, data)

    var (
        clientOpts = smtp.ClientOptions{
            ServerUrl:     fmt.Sprintf(`smtps://%s:%s`, smtpServer, smtpServerPort),
            AuthUser:      smtpUser,
            AuthPass:      smtpPass,
            AuthMechanism: smtp.SaslMechanismPlain,
        }
        cl *smtp.Client
    )

    cl, err = smtp.NewClient(clientOpts)
    if err != nil {
        log.Fatal(err)
    }

    res, err := cl.MailTx(mailtx)
    if err != nil {
        log.Fatal(`MailTx:`, err)
    }

    log.Printf(`MailTx: response: %+v`, res)

    _, err = cl.Quit()
    if err != nil {
        log.Fatal(err)
    }
}

What did you expect to see?

Full message received as,

from: <redacted>
to: <redacted>
subject: Truncated message
date: Sat, 8 Jul 2023 16:47:43 +0700
message-id: <1688809663.1vIVqALM@inspiro>
mime-version: 1.0
content-type: text/plain; charset="utf-8"
content-transfer-encoding: quoted-printable

A line that precisely have length 75 with . + LF will cause DATA truncation=
=2E

This line will not be received.

What did you see instead?

Message received as,

from: <redacted>
to: <redacted>
subject: Truncated message
date: Sat, 8 Jul 2023 16:38:41 +0700
message-id: <1688809121.Iz9VWWN7@inspiro>
mime-version: 1.0
content-type: text/plain; charset="utf-8"
content-transfer-encoding: quoted-printable

A line that precisely have length 75 with . + LF will cause DATA truncation=
gopherbot commented 1 year ago

Change https://go.dev/cl/508535 mentions this issue: mime/quotedprintable: fix encoding where a period alone on a line

neild commented 1 year ago

If we pass the Writer output into SMTP DATA command, the server read the "\r\n.\r\n" as the end of DATA which cause the message truncated on the receiver.

A line starting with a . needs to be sent to the SMTP DATA command prefixed with an additional .: https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.2

Perhaps we should change the mime/quotedprintable behavior, but the problem of message truncation here lies in the SMTP client implementation.

shuLhan commented 1 year ago

@neild Thanks for reviewing.

... but the problem of message truncation here lies in the SMTP client implementation.

You were right. I have tested with net/smtp Client and it works perfectly.

Perhaps we should change the mime/quotedprintable behavior

I read your comment on gerrit, will continue discussion in there.