mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-28 15:02:28 +00:00
8d763f450d
This is a follow-up on the `go fix` run in commit
0210c5fd92.
That commit recorded the automated updates.
This one applies further simplifications that are mechanical and do not
involve logic changes, but are outside of what `go fix` did.
It also removes some now-obsolete comments and statements.
112 lines
3.0 KiB
Go
112 lines
3.0 KiB
Go
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 errors.Is(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
|
|
}
|