package smtpsrv
import (
"bufio"
"bytes"
"errors"
"io"
)
var (
// TODO: Include the line number and specific error, and have the
// caller add them to the trace.
errMessageTooLarge = errors.New("message too large")
errInvalidLineEnding = errors.New("invalid line ending")
)
// readUntilDot reads from r until it encounters a dot-terminated line, or we
// read max bytes. It enforces that input lines are terminated by "\r\n", and
// that there are not "lonely" "\r" or "\n"s in the input.
// It returns \n-terminated lines, which is what we use for our internal
// representation for convenience (same as textproto DotReader does).
func readUntilDot(r *bufio.Reader, max int64) ([]byte, error) {
buf := make([]byte, 0, 1024)
n := int64(0)
// Little state machine.
const (
prevOther = iota
prevCR
prevCRLF
)
// Start as if we just came from a '\r\n'; that way we avoid the need
// for special-casing the dot-stuffing at the very beginning.
prev := prevCRLF
last4 := make([]byte, 4)
skip := false
loop:
for {
b, err := r.ReadByte()
if err == io.EOF {
return buf, io.ErrUnexpectedEOF
} else if err != nil {
return buf, err
}
n++
switch b {
case '\r':
if prev == prevCR {
return buf, errInvalidLineEnding
}
prev = prevCR
// We return a LF-terminated line, so skip the CR. This simplifies
// internal representation and makes it easier/less error prone to
// work with. It is converted back to CRLF on endpoints (e.g. in
// the couriers).
skip = true
case '\n':
if prev != prevCR {
return buf, errInvalidLineEnding
}
// If we come from a '\r\n.\r', we're done.
if bytes.Equal(last4, []byte("\r\n.\r")) {
break loop
}
// If we are only starting and see ".\r\n", we're also done; in
// that case the message is empty.
if n == 3 && bytes.Equal(last4, []byte("\x00\x00.\r")) {
return []byte{}, nil
}
prev = prevCRLF
default:
if prev == prevCR {
return buf, errInvalidLineEnding
}
if b == '.' && prev == prevCRLF {
// We come from "\r\n" and got a "."; as per dot-stuffing
// rules, we should skip that '.' in the output.
// https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
skip = true
}
prev = prevOther
}
// Keep the last 4 bytes separately, because they may not be in buf on
// messages that are too large.
copy(last4, last4[1:])
last4[3] = b
if int64(len(buf)) < max && !skip {
buf = append(buf, b)
}
skip = false
}
// Return an error if the message is too large. It is important to do this
// _outside_ the loop, because we need to keep reading until we get to the
// final "." before we return an error, so the SMTP dialog can continue
// properly after that.
// If we return too early, the remainder of the email is interpreted as
// part of the SMTP dialog (and exposing ourselves to smuggling attacks).
if n > max {
return buf, errMessageTooLarge
}
// If we made it this far, buf naturally ends in "\n" because we skipped
// the '.' due to dot-stuffing, and skip "\r"s.
return buf, nil
}