Class: MailSmtp::DataReader

Inherits:
Object
  • Object
show all
Defined in:
lib/mailsmtp/data_reader.rb

Overview

IO-like view of an SMTP DATA body. Yields dot-unstuffed bytes as the underlying connection produces lines; ends at the terminating "." line. SIZE and per-line limits set #failure and raise on #read / #readpartial (never return a truncated body as success). #drain consumes through "." without raising so the session can reply once, but still records SIZE/line failures (and an absolute byte ceiling when SIZE is nil) so a no-op receive cannot turn an oversize body into 250. Drain always stops once that ceiling is hit — unbounded drain would let a fast sender burn the link.

readpartial fills up to maxlen (or the terminating "."), so a mid-DATA disconnect raises ServiceUnavailable instead of returning a short chunk that apps treat as end-of-message. EOFError is raised only after the ".".

Constant Summary collapse

DRAIN_BYTE_CEILING =

Absolute drain ceiling when the server has no SIZE limit configured. Keep in sync with Server::DEFAULT_MAX_MESSAGE_SIZE (this file loads first).

10_485_760

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_message_size: nil, max_line_length: Server::MAX_TEXT_LINE_LENGTH, on_headers: nil, &read_line) ⇒ DataReader

Returns a new instance of DataReader.

Raises:

  • (ArgumentError)


23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/mailsmtp/data_reader.rb', line 23

def initialize(max_message_size: nil, max_line_length: Server::MAX_TEXT_LINE_LENGTH,
  on_headers: nil, &read_line)
  raise ArgumentError, "read_line block required" unless read_line

  @read_line = read_line
  @max_message_size = max_message_size
  @max_line_length = max_line_length
  @on_headers = on_headers
  @buffer = +"".b
  @bytes_seen = 0
  @eof = false
  @failure = nil
  @draining = false
  @headers_seen = false
  @drain_ceiling_hit = false
end

Instance Attribute Details

#failureObject (readonly)

Returns the value of attribute failure.



21
22
23
# File 'lib/mailsmtp/data_reader.rb', line 21

def failure
  @failure
end

Instance Method Details

#binmodeObject



111
# File 'lib/mailsmtp/data_reader.rb', line 111

def binmode = self

#closeObject



112
# File 'lib/mailsmtp/data_reader.rb', line 112

def close = nil

#closed?Boolean

Returns:

  • (Boolean)


113
# File 'lib/mailsmtp/data_reader.rb', line 113

def closed? = false

#drainObject



101
102
103
104
105
106
107
# File 'lib/mailsmtp/data_reader.rb', line 101

def drain
  @draining = true
  @buffer = +""
  # Stop on SIZE, or on the absolute drain ceiling when SIZE is off / after
  # LineTooLong — never read forever waiting for ".".
  fill_until { @drain_ceiling_hit || @failure.is_a?(MessageTooLarge) }
end

#drain_ceiling_hit?Boolean

Returns:

  • (Boolean)


109
# File 'lib/mailsmtp/data_reader.rb', line 109

def drain_ceiling_hit? = @drain_ceiling_hit

#eof?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/mailsmtp/data_reader.rb', line 97

def eof?
  @eof && @buffer.empty? && (@draining || @failure.nil?)
end

#read(length = nil, outbuf = nil) ⇒ Object

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/mailsmtp/data_reader.rb', line 40

def read(length = nil, outbuf = nil)
  raise @failure if @failure && !@draining

  # IO#read(0) returns an empty string without consuming input.
  if length == 0
    chunk = +""
    return outbuf ? outbuf.replace(chunk) : chunk
  end

  if length
    fill_until { @buffer.bytesize >= length || @eof || @failure }
  else
    fill_until { @eof || @failure }
  end

  raise @failure if @failure && !@draining

  if @buffer.empty?
    # IO#read(length) returns nil at EOF; read() with no length returns "".
    chunk = length ? nil : +""
  elsif length
    chunk = @buffer.slice!(0, length)
  else
    chunk = @buffer
    @buffer = +""
  end

  if outbuf
    outbuf.replace(chunk || +"")
    chunk
  else
    chunk
  end
end

#readpartial(maxlen, outbuf = nil) ⇒ Object

Raises:

  • (ArgumentError)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/mailsmtp/data_reader.rb', line 75

def readpartial(maxlen, outbuf = nil)
  raise ArgumentError, "maxlen must be positive" unless maxlen.positive?
  raise @failure if @failure && !@draining

  # Fill up to maxlen (not "any bytes") so a peer drop is discovered on this
  # call — returning the first line alone lets short-read loops treat a
  # truncated body as complete while the engine later 451s / redelivers.
  fill_until { @buffer.bytesize >= maxlen || @eof || @failure }

  raise @failure if @failure && !@draining
  if @buffer.empty?
    # EOFError only after the terminating "."; a drop mid-DATA must not look
    # like a clean end-of-message (apps that rescue EOFError would accept a
    # truncated body and the engine would map that to LocalError / 451).
    raise EOFError, "end of data" if @eof
    raise ServiceUnavailable, "Connection lost during DATA"
  end

  chunk = @buffer.slice!(0, maxlen)
  outbuf ? outbuf.replace(chunk) : chunk
end