mirror of
https://github.com/mjl-/mox.git
synced 2025-07-13 11:34:37 +03:00
add strict mode when parsing messages, typically enabled for incoming special-use messages like tls/dmarc reports, subjectpass emails
and pass a logger to the message parser, so problems with message parsing get the cid logged.
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"net/textproto"
|
||||
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/smtp"
|
||||
)
|
||||
|
||||
@ -16,12 +17,12 @@ import (
|
||||
// From headers may be present. From returns an error if there is not exactly
|
||||
// one address. This address can be used for evaluating a DMARC policy against
|
||||
// SPF and DKIM results.
|
||||
func From(r io.ReaderAt) (raddr smtp.Address, header textproto.MIMEHeader, rerr error) {
|
||||
func From(log *mlog.Log, strict bool, r io.ReaderAt) (raddr smtp.Address, header textproto.MIMEHeader, rerr error) {
|
||||
// ../rfc/7489:1243
|
||||
|
||||
// todo: only allow utf8 if enabled in session/message?
|
||||
|
||||
p, err := Parse(r)
|
||||
p, err := Parse(log, strict, r)
|
||||
if err != nil {
|
||||
// todo: should we continue with p, perhaps headers can be parsed?
|
||||
return raddr, nil, fmt.Errorf("parsing message: %v", err)
|
||||
|
152
message/part.go
152
message/part.go
@ -7,7 +7,6 @@ package message
|
||||
// todo: should we allow base64 messages where a line starts with a space? and possibly more whitespace. is happening in messages. coreutils base64 accepts it, encoding/base64 does not.
|
||||
// todo: handle comments in headers?
|
||||
// todo: should we just always store messages with \n instead of \r\n? \r\n seems easier for use with imap.
|
||||
// todo: is a header always \r\n\r\n-separated? or is \r\n enough at the beginning of a file? because what would this mean: "\r\ndata"? data isn't a header.
|
||||
// todo: can use a cleanup
|
||||
|
||||
import (
|
||||
@ -32,8 +31,6 @@ import (
|
||||
"github.com/mjl-/mox/smtp"
|
||||
)
|
||||
|
||||
var xlog = mlog.New("message")
|
||||
|
||||
var (
|
||||
ErrBadContentType = errors.New("bad content-type")
|
||||
)
|
||||
@ -83,6 +80,7 @@ type Part struct {
|
||||
lastBoundOffset int64 // Start of header of last/previous part. Used to skip a part if ParseNextPart is called and nextBoundOffset is -1.
|
||||
parent *Part // Parent part, for getting bound from, and setting nextBoundOffset when a part has finished reading. Only for subparts, not top-level parts.
|
||||
bound []byte // Only set if valid multipart with boundary, includes leading --, excludes \r\n.
|
||||
strict bool // If set, valid crlf line endings are verified when reading body.
|
||||
}
|
||||
|
||||
// todo: have all Content* fields in Part?
|
||||
@ -112,18 +110,24 @@ type Address struct {
|
||||
|
||||
// Parse reads the headers of the mail message and returns a part.
|
||||
// A part provides access to decoded and raw contents of a message and its multiple parts.
|
||||
func Parse(r io.ReaderAt) (Part, error) {
|
||||
return newPart(r, 0, nil)
|
||||
//
|
||||
// If strict is set, fewer attempts are made to continue parsing when errors are
|
||||
// encountered, such as with invalid content-type headers or bare carriage returns.
|
||||
func Parse(log *mlog.Log, strict bool, r io.ReaderAt) (Part, error) {
|
||||
return newPart(log, strict, r, 0, nil)
|
||||
}
|
||||
|
||||
// EnsurePart parses a part as with Parse, but ensures a usable part is always
|
||||
// returned, even if error is non-nil. If a parse error occurs, the message is
|
||||
// returned as application/octet-stream, and headers can still be read if they
|
||||
// were valid.
|
||||
func EnsurePart(r io.ReaderAt, size int64) (Part, error) {
|
||||
p, err := Parse(r)
|
||||
//
|
||||
// If strict is set, fewer attempts are made to continue parsing when errors are
|
||||
// encountered, such as with invalid content-type headers or bare carriage returns.
|
||||
func EnsurePart(log *mlog.Log, strict bool, r io.ReaderAt, size int64) (Part, error) {
|
||||
p, err := Parse(log, strict, r)
|
||||
if err == nil {
|
||||
err = p.Walk(nil)
|
||||
err = p.Walk(log, nil)
|
||||
}
|
||||
if err != nil {
|
||||
np, err2 := fallbackPart(p, r, size)
|
||||
@ -183,7 +187,7 @@ func (p *Part) SetMessageReaderAt() error {
|
||||
}
|
||||
|
||||
// Walk through message, decoding along the way, and collecting mime part offsets and sizes, and line counts.
|
||||
func (p *Part) Walk(parent *Part) error {
|
||||
func (p *Part) Walk(log *mlog.Log, parent *Part) error {
|
||||
if len(p.bound) == 0 {
|
||||
if p.MediaType == "MESSAGE" && (p.MediaSubType == "RFC822" || p.MediaSubType == "GLOBAL") {
|
||||
// todo: don't read whole submessage in memory...
|
||||
@ -192,11 +196,11 @@ func (p *Part) Walk(parent *Part) error {
|
||||
return err
|
||||
}
|
||||
br := bytes.NewReader(buf)
|
||||
mp, err := Parse(br)
|
||||
mp, err := Parse(log, p.strict, br)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing embedded message: %w", err)
|
||||
}
|
||||
if err := mp.Walk(nil); err != nil {
|
||||
if err := mp.Walk(log, nil); err != nil {
|
||||
// If this is a DSN and we are not in pedantic mode, accept unexpected end of
|
||||
// message. This is quite common because MTA's sometimes just truncate the original
|
||||
// message in a place that makes the message invalid.
|
||||
@ -218,14 +222,14 @@ func (p *Part) Walk(parent *Part) error {
|
||||
}
|
||||
|
||||
for {
|
||||
pp, err := p.ParseNextPart()
|
||||
pp, err := p.ParseNextPart(log)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pp.Walk(p); err != nil {
|
||||
if err := pp.Walk(log, p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -239,7 +243,7 @@ func (p *Part) String() string {
|
||||
// newPart parses a new part, which can be the top-level message.
|
||||
// offset is the bound offset for parts, and the start of message for top-level messages. parent indicates if this is a top-level message or sub-part.
|
||||
// If an error occurs, p's exported values can still be relevant. EnsurePart uses these values.
|
||||
func newPart(r io.ReaderAt, offset int64, parent *Part) (p Part, rerr error) {
|
||||
func newPart(log *mlog.Log, strict bool, r io.ReaderAt, offset int64, parent *Part) (p Part, rerr error) {
|
||||
if r == nil {
|
||||
panic("nil reader")
|
||||
}
|
||||
@ -248,9 +252,10 @@ func newPart(r io.ReaderAt, offset int64, parent *Part) (p Part, rerr error) {
|
||||
EndOffset: -1,
|
||||
r: r,
|
||||
parent: parent,
|
||||
strict: strict,
|
||||
}
|
||||
|
||||
b := &bufAt{r: r, offset: offset}
|
||||
b := &bufAt{strict: strict, r: r, offset: offset}
|
||||
|
||||
if parent != nil {
|
||||
p.BoundaryOffset = offset
|
||||
@ -297,16 +302,46 @@ func newPart(r io.ReaderAt, offset int64, parent *Part) (p Part, rerr error) {
|
||||
ct := p.header.Get("Content-Type")
|
||||
mt, params, err := mime.ParseMediaType(ct)
|
||||
if err != nil && ct != "" {
|
||||
return p, fmt.Errorf("%w: %s: %q", ErrBadContentType, err, ct)
|
||||
}
|
||||
if mt != "" {
|
||||
if moxvar.Pedantic || strict {
|
||||
return p, fmt.Errorf("%w: %s: %q", ErrBadContentType, err, ct)
|
||||
}
|
||||
|
||||
// Try parsing just a content-type, ignoring parameters.
|
||||
// ../rfc/2045:628
|
||||
ct = strings.TrimSpace(strings.SplitN(ct, ";", 2)[0])
|
||||
t := strings.SplitN(ct, "/", 2)
|
||||
isToken := func(s string) bool {
|
||||
const separators = `()<>@,;:\\"/[]?= ` // ../rfc/2045:663
|
||||
for _, c := range s {
|
||||
if c < 0x20 || c >= 0x80 || strings.ContainsRune(separators, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
// We cannot recover content-type of multipart, we won't have a boundary.
|
||||
if len(t) == 2 && isToken(t[0]) && !strings.EqualFold(t[0], "multipart") && isToken(t[1]) {
|
||||
p.MediaType = strings.ToUpper(t[0])
|
||||
p.MediaSubType = strings.ToUpper(t[1])
|
||||
} else {
|
||||
p.MediaType = "APPLICATION"
|
||||
p.MediaSubType = "OCTET-STREAM"
|
||||
}
|
||||
log.Debugx("malformed content-type, attempting to recover and continuing", err, mlog.Field("contenttype", p.header.Get("Content-Type")), mlog.Field("mediatype", p.MediaType), mlog.Field("mediasubtype", p.MediaSubType))
|
||||
} else if mt != "" {
|
||||
t := strings.SplitN(strings.ToUpper(mt), "/", 2)
|
||||
if len(t) != 2 {
|
||||
return p, fmt.Errorf("bad content-type: %q (content-type %q)", mt, ct)
|
||||
if moxvar.Pedantic || strict {
|
||||
return p, fmt.Errorf("bad content-type: %q (content-type %q)", mt, ct)
|
||||
}
|
||||
log.Debug("malformed media-type, ignoring and continuing", mlog.Field("type", mt))
|
||||
p.MediaType = "APPLICATION"
|
||||
p.MediaSubType = "OCTET-STREAM"
|
||||
} else {
|
||||
p.MediaType = t[0]
|
||||
p.MediaSubType = t[1]
|
||||
p.ContentTypeParams = params
|
||||
}
|
||||
p.MediaType = t[0]
|
||||
p.MediaSubType = t[1]
|
||||
p.ContentTypeParams = params
|
||||
}
|
||||
|
||||
p.ContentID = p.header.Get("Content-Id")
|
||||
@ -314,7 +349,7 @@ func newPart(r io.ReaderAt, offset int64, parent *Part) (p Part, rerr error) {
|
||||
p.ContentTransferEncoding = strings.ToUpper(p.header.Get("Content-Transfer-Encoding"))
|
||||
|
||||
if parent == nil {
|
||||
p.Envelope, err = parseEnvelope(mail.Header(p.header))
|
||||
p.Envelope, err = parseEnvelope(log, mail.Header(p.header))
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
@ -411,7 +446,7 @@ var wordDecoder = mime.WordDecoder{
|
||||
},
|
||||
}
|
||||
|
||||
func parseEnvelope(h mail.Header) (*Envelope, error) {
|
||||
func parseEnvelope(log *mlog.Log, h mail.Header) (*Envelope, error) {
|
||||
date, _ := h.Date()
|
||||
|
||||
// We currently marshal this field to JSON. But JSON cannot represent all
|
||||
@ -433,19 +468,19 @@ func parseEnvelope(h mail.Header) (*Envelope, error) {
|
||||
env := &Envelope{
|
||||
date,
|
||||
subject,
|
||||
parseAddressList(h, "from"),
|
||||
parseAddressList(h, "sender"),
|
||||
parseAddressList(h, "reply-to"),
|
||||
parseAddressList(h, "to"),
|
||||
parseAddressList(h, "cc"),
|
||||
parseAddressList(h, "bcc"),
|
||||
parseAddressList(log, h, "from"),
|
||||
parseAddressList(log, h, "sender"),
|
||||
parseAddressList(log, h, "reply-to"),
|
||||
parseAddressList(log, h, "to"),
|
||||
parseAddressList(log, h, "cc"),
|
||||
parseAddressList(log, h, "bcc"),
|
||||
h.Get("In-Reply-To"),
|
||||
h.Get("Message-Id"),
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func parseAddressList(h mail.Header, k string) []Address {
|
||||
func parseAddressList(log *mlog.Log, h mail.Header, k string) []Address {
|
||||
l, err := h.AddressList(k)
|
||||
if err != nil {
|
||||
return nil
|
||||
@ -457,7 +492,7 @@ func parseAddressList(h mail.Header, k string) []Address {
|
||||
addr, err := smtp.ParseAddress(a.Address)
|
||||
if err != nil {
|
||||
// todo: pass a ctx to this function so we can log with cid.
|
||||
xlog.Infox("parsing address", err, mlog.Field("address", a.Address))
|
||||
log.Infox("parsing address (continuing)", err, mlog.Field("address", a.Address))
|
||||
} else {
|
||||
user = addr.Localpart.String()
|
||||
host = addr.Domain.ASCII
|
||||
@ -470,7 +505,7 @@ func parseAddressList(h mail.Header, k string) []Address {
|
||||
// ParseNextPart parses the next (sub)part of this multipart message.
|
||||
// ParseNextPart returns io.EOF and a nil part when there are no more parts.
|
||||
// Only used for initial parsing of message. Once parsed, use p.Parts.
|
||||
func (p *Part) ParseNextPart() (*Part, error) {
|
||||
func (p *Part) ParseNextPart(log *mlog.Log) (*Part, error) {
|
||||
if len(p.bound) == 0 {
|
||||
return nil, errNotMultipart
|
||||
}
|
||||
@ -479,7 +514,7 @@ func (p *Part) ParseNextPart() (*Part, error) {
|
||||
panic("access not sequential")
|
||||
}
|
||||
// Set nextBoundOffset by fully reading the last part.
|
||||
last, err := newPart(p.r, p.lastBoundOffset, p)
|
||||
last, err := newPart(log, p.strict, p.r, p.lastBoundOffset, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -490,7 +525,7 @@ func (p *Part) ParseNextPart() (*Part, error) {
|
||||
return nil, fmt.Errorf("internal error: reading part did not set nextBoundOffset")
|
||||
}
|
||||
}
|
||||
b := &bufAt{r: p.r, offset: p.nextBoundOffset}
|
||||
b := &bufAt{strict: p.strict, r: p.r, offset: p.nextBoundOffset}
|
||||
// todo: should we require a crlf on final closing bound? we don't require it because some message/rfc822 don't have a crlf after their closing boundary, so those messages don't end in crlf.
|
||||
line, crlf, err := b.ReadLine(false)
|
||||
if err != nil {
|
||||
@ -523,7 +558,7 @@ func (p *Part) ParseNextPart() (*Part, error) {
|
||||
boundOffset := p.nextBoundOffset
|
||||
p.lastBoundOffset = boundOffset
|
||||
p.nextBoundOffset = -1
|
||||
np, err := newPart(p.r, boundOffset, p)
|
||||
np, err := newPart(log, p.strict, p.r, boundOffset, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -623,13 +658,37 @@ func (p *Part) RawReader() io.Reader {
|
||||
panic("missing reader")
|
||||
}
|
||||
if p.EndOffset >= 0 {
|
||||
return io.NewSectionReader(p.r, p.BodyOffset, p.EndOffset-p.BodyOffset)
|
||||
return &crlfReader{strict: p.strict, r: io.NewSectionReader(p.r, p.BodyOffset, p.EndOffset-p.BodyOffset)}
|
||||
}
|
||||
p.RawLineCount = 0
|
||||
if p.parent == nil {
|
||||
return &offsetReader{p, p.BodyOffset, true, false}
|
||||
return &offsetReader{p, p.BodyOffset, p.strict, true, false}
|
||||
}
|
||||
return &boundReader{p: p, b: &bufAt{r: p.r, offset: p.BodyOffset}, prevlf: true}
|
||||
return &boundReader{p: p, b: &bufAt{strict: p.strict, r: p.r, offset: p.BodyOffset}, prevlf: true}
|
||||
}
|
||||
|
||||
// crlfReader verifies there are no bare newlines and optionally no bare carriage returns.
|
||||
type crlfReader struct {
|
||||
r io.Reader
|
||||
strict bool
|
||||
prevcr bool
|
||||
}
|
||||
|
||||
func (r *crlfReader) Read(buf []byte) (int, error) {
|
||||
n, err := r.r.Read(buf)
|
||||
if err == nil || err == io.EOF {
|
||||
for _, b := range buf[:n] {
|
||||
if b == '\n' && !r.prevcr {
|
||||
err = errBareLF
|
||||
break
|
||||
} else if b != '\n' && r.prevcr && (r.strict || moxvar.Pedantic) {
|
||||
err = errBareCR
|
||||
break
|
||||
}
|
||||
r.prevcr = b == '\r'
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// bufAt is a buffered reader on an underlying ReaderAt.
|
||||
@ -637,6 +696,7 @@ func (p *Part) RawReader() io.Reader {
|
||||
type bufAt struct {
|
||||
offset int64 // Offset in r currently consumed, i.e. not including any buffered data.
|
||||
|
||||
strict bool
|
||||
r io.ReaderAt
|
||||
buf []byte // Buffered data.
|
||||
nbuf int // Valid bytes in buf.
|
||||
@ -698,7 +758,7 @@ func (b *bufAt) line(consume, requirecrlf bool) (buf []byte, crlf bool, err erro
|
||||
}
|
||||
i++
|
||||
if i >= b.nbuf || b.buf[i] != '\n' {
|
||||
if moxvar.Pedantic {
|
||||
if b.strict || moxvar.Pedantic {
|
||||
return nil, false, errBareCR
|
||||
}
|
||||
continue
|
||||
@ -743,6 +803,7 @@ func (b *bufAt) PeekByte() (byte, error) {
|
||||
type offsetReader struct {
|
||||
p *Part
|
||||
offset int64
|
||||
strict bool
|
||||
prevlf bool
|
||||
prevcr bool
|
||||
}
|
||||
@ -759,7 +820,7 @@ func (r *offsetReader) Read(buf []byte) (int, error) {
|
||||
if err == nil || err == io.EOF {
|
||||
if c == '\n' && !r.prevcr {
|
||||
err = errBareLF
|
||||
} else if c != '\n' && r.prevcr && moxvar.Pedantic {
|
||||
} else if c != '\n' && r.prevcr && (r.strict || moxvar.Pedantic) {
|
||||
err = errBareCR
|
||||
}
|
||||
}
|
||||
@ -784,7 +845,6 @@ type boundReader struct {
|
||||
nbuf int // Number of valid bytes in buf.
|
||||
crlf []byte // Possible crlf, to be returned if we do not yet encounter a boundary.
|
||||
prevlf bool // If last char returned was a newline. For counting lines.
|
||||
prevcr bool
|
||||
}
|
||||
|
||||
func (b *boundReader) Read(buf []byte) (count int, rerr error) {
|
||||
@ -795,15 +855,7 @@ func (b *boundReader) Read(buf []byte) (count int, rerr error) {
|
||||
if b.prevlf {
|
||||
b.p.RawLineCount++
|
||||
}
|
||||
if rerr == nil || rerr == io.EOF {
|
||||
if c == '\n' && !b.prevcr {
|
||||
rerr = errBareLF
|
||||
} else if c != '\n' && b.prevcr && moxvar.Pedantic {
|
||||
rerr = errBareCR
|
||||
}
|
||||
}
|
||||
b.prevlf = c == '\n'
|
||||
b.prevcr = c == '\r'
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -11,9 +11,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/moxvar"
|
||||
)
|
||||
|
||||
var xlog = mlog.New("message")
|
||||
|
||||
func tcheck(t *testing.T, err error, msg string) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
@ -37,7 +40,7 @@ func tfail(t *testing.T, err, expErr error) {
|
||||
|
||||
func TestEmptyHeader(t *testing.T) {
|
||||
s := "\r\nx"
|
||||
p, err := EnsurePart(strings.NewReader(s), int64(len(s)))
|
||||
p, err := EnsurePart(xlog, true, strings.NewReader(s), int64(len(s)))
|
||||
tcheck(t, err, "parse empty headers")
|
||||
buf, err := io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
@ -48,15 +51,85 @@ func TestEmptyHeader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBadContentType(t *testing.T) {
|
||||
expBody := "test"
|
||||
|
||||
// Pedantic is like strict.
|
||||
moxvar.Pedantic = true
|
||||
s := "content-type: text/html;;\r\n\r\ntest"
|
||||
p, err := EnsurePart(strings.NewReader(s), int64(len(s)))
|
||||
p, err := EnsurePart(xlog, false, strings.NewReader(s), int64(len(s)))
|
||||
tfail(t, err, ErrBadContentType)
|
||||
buf, err := io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
expBody := "test"
|
||||
tcompare(t, string(buf), expBody)
|
||||
tcompare(t, p.MediaType, "APPLICATION")
|
||||
tcompare(t, p.MediaSubType, "OCTET-STREAM")
|
||||
moxvar.Pedantic = false
|
||||
|
||||
// Strict
|
||||
s = "content-type: text/html;;\r\n\r\ntest"
|
||||
p, err = EnsurePart(xlog, true, strings.NewReader(s), int64(len(s)))
|
||||
tfail(t, err, ErrBadContentType)
|
||||
buf, err = io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
tcompare(t, string(buf), expBody)
|
||||
tcompare(t, p.MediaType, "APPLICATION")
|
||||
tcompare(t, p.MediaSubType, "OCTET-STREAM")
|
||||
|
||||
// Non-strict but unrecoverable content-type.
|
||||
s = "content-type: not a content type;;\r\n\r\ntest"
|
||||
p, err = EnsurePart(xlog, false, strings.NewReader(s), int64(len(s)))
|
||||
tcheck(t, err, "parsing message with bad but recoverable content-type")
|
||||
buf, err = io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
tcompare(t, string(buf), expBody)
|
||||
tcompare(t, p.MediaType, "APPLICATION")
|
||||
tcompare(t, p.MediaSubType, "OCTET-STREAM")
|
||||
|
||||
// We try to use only the content-type, typically better than application/octet-stream.
|
||||
s = "content-type: text/html;;\r\n\r\ntest"
|
||||
p, err = EnsurePart(xlog, false, strings.NewReader(s), int64(len(s)))
|
||||
tcheck(t, err, "parsing message with bad but recoverable content-type")
|
||||
buf, err = io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
tcompare(t, string(buf), expBody)
|
||||
tcompare(t, p.MediaType, "TEXT")
|
||||
tcompare(t, p.MediaSubType, "HTML")
|
||||
|
||||
// Not recovering multipart, we won't have a boundary.
|
||||
s = "content-type: multipart/mixed;;\r\n\r\ntest"
|
||||
p, err = EnsurePart(xlog, false, strings.NewReader(s), int64(len(s)))
|
||||
tcheck(t, err, "parsing message with bad but recoverable content-type")
|
||||
buf, err = io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
tcompare(t, string(buf), expBody)
|
||||
tcompare(t, p.MediaType, "APPLICATION")
|
||||
tcompare(t, p.MediaSubType, "OCTET-STREAM")
|
||||
}
|
||||
|
||||
func TestBareCR(t *testing.T) {
|
||||
s := "content-type: text/html\r\n\r\nbare\rcr\r\n"
|
||||
expBody := "bare\rcr\r\n"
|
||||
|
||||
// Pedantic is like strict.
|
||||
moxvar.Pedantic = true
|
||||
p, err := EnsurePart(xlog, false, strings.NewReader(s), int64(len(s)))
|
||||
tfail(t, err, errBareCR)
|
||||
_, err = io.ReadAll(p.Reader())
|
||||
tfail(t, err, errBareCR)
|
||||
moxvar.Pedantic = false
|
||||
|
||||
// Strict.
|
||||
p, err = EnsurePart(xlog, true, strings.NewReader(s), int64(len(s)))
|
||||
tfail(t, err, errBareCR)
|
||||
_, err = io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read fallback part without error")
|
||||
|
||||
// Non-strict allows bare cr.
|
||||
p, err = EnsurePart(xlog, false, strings.NewReader(s), int64(len(s)))
|
||||
tcheck(t, err, "parse")
|
||||
buf, err := io.ReadAll(p.Reader())
|
||||
tcheck(t, err, "read")
|
||||
tcompare(t, string(buf), expBody)
|
||||
}
|
||||
|
||||
var basicMsg = strings.ReplaceAll(`From: <mjl@mox.example>
|
||||
@ -68,7 +141,7 @@ aGkK
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
r := strings.NewReader(basicMsg)
|
||||
p, err := Parse(r)
|
||||
p, err := Parse(xlog, true, r)
|
||||
tcheck(t, err, "new reader")
|
||||
|
||||
buf, err := io.ReadAll(p.RawReader())
|
||||
@ -103,7 +176,7 @@ Hello Joe, do you think we can meet at 3:30 tomorrow?
|
||||
|
||||
func TestBasic2(t *testing.T) {
|
||||
r := strings.NewReader(basicMsg2)
|
||||
p, err := Parse(r)
|
||||
p, err := Parse(xlog, true, r)
|
||||
tcheck(t, err, "new reader")
|
||||
|
||||
buf, err := io.ReadAll(p.RawReader())
|
||||
@ -123,9 +196,9 @@ func TestBasic2(t *testing.T) {
|
||||
}
|
||||
|
||||
r = strings.NewReader(basicMsg2)
|
||||
p, err = Parse(r)
|
||||
p, err = Parse(xlog, true, r)
|
||||
tcheck(t, err, "new reader")
|
||||
err = p.Walk(nil)
|
||||
err = p.Walk(xlog, nil)
|
||||
tcheck(t, err, "walk")
|
||||
if p.RawLineCount != 2 {
|
||||
t.Fatalf("basic message, got %d lines, expected 2", p.RawLineCount)
|
||||
@ -164,25 +237,25 @@ This is the epilogue. It is also to be ignored.
|
||||
func TestMime(t *testing.T) {
|
||||
// from ../rfc/2046:1148
|
||||
r := strings.NewReader(mimeMsg)
|
||||
p, err := Parse(r)
|
||||
p, err := Parse(xlog, true, r)
|
||||
tcheck(t, err, "new reader")
|
||||
if len(p.bound) == 0 {
|
||||
t.Fatalf("got no bound, expected bound for mime message")
|
||||
}
|
||||
|
||||
pp, err := p.ParseNextPart()
|
||||
pp, err := p.ParseNextPart(xlog)
|
||||
tcheck(t, err, "next part")
|
||||
buf, err := io.ReadAll(pp.Reader())
|
||||
tcheck(t, err, "read all")
|
||||
tcompare(t, string(buf), "This is implicitly typed plain US-ASCII text.\r\nIt does NOT end with a linebreak.")
|
||||
|
||||
pp, err = p.ParseNextPart()
|
||||
pp, err = p.ParseNextPart(xlog)
|
||||
tcheck(t, err, "next part")
|
||||
buf, err = io.ReadAll(pp.Reader())
|
||||
tcheck(t, err, "read all")
|
||||
tcompare(t, string(buf), "This is explicitly typed plain US-ASCII text.\r\nIt DOES end with a linebreak.\r\n")
|
||||
|
||||
_, err = p.ParseNextPart()
|
||||
_, err = p.ParseNextPart(xlog)
|
||||
tcompare(t, err, io.EOF)
|
||||
|
||||
if len(p.Parts) != 2 {
|
||||
@ -201,33 +274,38 @@ func TestLongLine(t *testing.T) {
|
||||
for i := range line {
|
||||
line[i] = 'a'
|
||||
}
|
||||
_, err := Parse(bytes.NewReader(line))
|
||||
_, err := Parse(xlog, true, bytes.NewReader(line))
|
||||
tfail(t, err, errLineTooLong)
|
||||
}
|
||||
|
||||
func TestHalfCrLf(t *testing.T) {
|
||||
parse := func(s string) error {
|
||||
p, err := Parse(strings.NewReader(s))
|
||||
func TestBareCrLf(t *testing.T) {
|
||||
parse := func(strict bool, s string) error {
|
||||
p, err := Parse(xlog, strict, strings.NewReader(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Walk(nil)
|
||||
return p.Walk(xlog, nil)
|
||||
}
|
||||
err := parse("subject: test\ntest\r\n")
|
||||
err := parse(false, "subject: test\ntest\r\n")
|
||||
tfail(t, err, errBareLF)
|
||||
err = parse("\r\ntest\ntest\r\n")
|
||||
err = parse(false, "\r\ntest\ntest\r\n")
|
||||
tfail(t, err, errBareLF)
|
||||
|
||||
moxvar.Pedantic = true
|
||||
err = parse("subject: test\rtest\r\n")
|
||||
err = parse(false, "subject: test\rtest\r\n")
|
||||
tfail(t, err, errBareCR)
|
||||
err = parse("\r\ntest\rtest\r\n")
|
||||
err = parse(false, "\r\ntest\rtest\r\n")
|
||||
tfail(t, err, errBareCR)
|
||||
moxvar.Pedantic = false
|
||||
|
||||
err = parse("subject: test\rtest\r\n")
|
||||
err = parse(true, "subject: test\rtest\r\n")
|
||||
tfail(t, err, errBareCR)
|
||||
err = parse(true, "\r\ntest\rtest\r\n")
|
||||
tfail(t, err, errBareCR)
|
||||
|
||||
err = parse(false, "subject: test\rtest\r\n")
|
||||
tcheck(t, err, "header with bare cr")
|
||||
err = parse("\r\ntest\rtest\r\n")
|
||||
err = parse(false, "\r\ntest\rtest\r\n")
|
||||
tcheck(t, err, "body with bare cr")
|
||||
}
|
||||
|
||||
@ -238,25 +316,25 @@ func TestMissingClosingBoundary(t *testing.T) {
|
||||
|
||||
test
|
||||
`, "\n", "\r\n")
|
||||
msg, err := Parse(strings.NewReader(message))
|
||||
msg, err := Parse(xlog, false, strings.NewReader(message))
|
||||
tcheck(t, err, "new reader")
|
||||
err = walkmsg(&msg)
|
||||
tfail(t, err, errMissingClosingBoundary)
|
||||
|
||||
msg, _ = Parse(strings.NewReader(message))
|
||||
err = msg.Walk(nil)
|
||||
msg, _ = Parse(xlog, false, strings.NewReader(message))
|
||||
err = msg.Walk(xlog, nil)
|
||||
tfail(t, err, errMissingClosingBoundary)
|
||||
}
|
||||
|
||||
func TestHeaderEOF(t *testing.T) {
|
||||
message := "header: test"
|
||||
_, err := Parse(strings.NewReader(message))
|
||||
_, err := Parse(xlog, false, strings.NewReader(message))
|
||||
tfail(t, err, errUnexpectedEOF)
|
||||
}
|
||||
|
||||
func TestBodyEOF(t *testing.T) {
|
||||
message := "header: test\r\n\r\ntest"
|
||||
msg, err := Parse(strings.NewReader(message))
|
||||
msg, err := Parse(xlog, true, strings.NewReader(message))
|
||||
tcheck(t, err, "new reader")
|
||||
buf, err := io.ReadAll(msg.Reader())
|
||||
tcheck(t, err, "read body")
|
||||
@ -287,7 +365,7 @@ test
|
||||
|
||||
`, "\n", "\r\n")
|
||||
|
||||
msg, err := Parse(strings.NewReader(message))
|
||||
msg, err := Parse(xlog, false, strings.NewReader(message))
|
||||
tcheck(t, err, "new reader")
|
||||
enforceSequential = true
|
||||
defer func() {
|
||||
@ -296,8 +374,8 @@ test
|
||||
err = walkmsg(&msg)
|
||||
tcheck(t, err, "walkmsg")
|
||||
|
||||
msg, _ = Parse(strings.NewReader(message))
|
||||
err = msg.Walk(nil)
|
||||
msg, _ = Parse(xlog, false, strings.NewReader(message))
|
||||
err = msg.Walk(xlog, nil)
|
||||
tcheck(t, err, "msg.Walk")
|
||||
}
|
||||
|
||||
@ -374,7 +452,7 @@ Content-Transfer-Encoding: Quoted-printable
|
||||
--unique-boundary-1--
|
||||
`, "\n", "\r\n")
|
||||
|
||||
msg, err := Parse(strings.NewReader(nestedMessage))
|
||||
msg, err := Parse(xlog, true, strings.NewReader(nestedMessage))
|
||||
tcheck(t, err, "new reader")
|
||||
enforceSequential = true
|
||||
defer func() {
|
||||
@ -399,8 +477,8 @@ Content-Transfer-Encoding: Quoted-printable
|
||||
t.Fatalf("got %q, expected %q", buf, exp)
|
||||
}
|
||||
|
||||
msg, _ = Parse(strings.NewReader(nestedMessage))
|
||||
err = msg.Walk(nil)
|
||||
msg, _ = Parse(xlog, false, strings.NewReader(nestedMessage))
|
||||
err = msg.Walk(xlog, nil)
|
||||
tcheck(t, err, "msg.Walk")
|
||||
|
||||
}
|
||||
@ -440,7 +518,7 @@ func walk(path string) error {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
msg, err := Parse(r)
|
||||
msg, err := Parse(xlog, false, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -460,7 +538,7 @@ func walkmsg(msg *Part) error {
|
||||
}
|
||||
|
||||
if msg.MediaType == "MESSAGE" && (msg.MediaSubType == "RFC822" || msg.MediaSubType == "GLOBAL") {
|
||||
mp, err := Parse(bytes.NewReader(buf))
|
||||
mp, err := Parse(xlog, false, bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -488,7 +566,7 @@ func walkmsg(msg *Part) error {
|
||||
}
|
||||
|
||||
for {
|
||||
pp, err := msg.ParseNextPart()
|
||||
pp, err := msg.ParseNextPart(xlog)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
@ -507,7 +585,7 @@ func TestEmbedded(t *testing.T) {
|
||||
tcheck(t, err, "open")
|
||||
fi, err := f.Stat()
|
||||
tcheck(t, err, "stat")
|
||||
_, err = EnsurePart(f, fi.Size())
|
||||
_, err = EnsurePart(xlog, false, f, fi.Size())
|
||||
tcheck(t, err, "parse")
|
||||
}
|
||||
|
||||
@ -516,6 +594,6 @@ func TestEmbedded2(t *testing.T) {
|
||||
tcheck(t, err, "readfile")
|
||||
buf = bytes.ReplaceAll(buf, []byte("\n"), []byte("\r\n"))
|
||||
|
||||
_, err = EnsurePart(bytes.NewReader(buf), int64(len(buf)))
|
||||
_, err = EnsurePart(xlog, false, bytes.NewReader(buf), int64(len(buf)))
|
||||
tfail(t, err, nil)
|
||||
}
|
||||
|
Reference in New Issue
Block a user