///|
/// One completed mboxrd record. Source offsets include envelope/quoting/separator;
/// content only removes reversible From quoting and ONE container blank line.
/// Bytes are owned snapshots, never aliases into the decoder's mutable buffers.
pub struct RawMessage {
number : Int64
source_start : Int64
source_end : Int64
envelope : Bytes
content : Bytes
} derive(Debug, Eq)
///|
/// Incremental container decoder, independent of MIME. One 1MiB input chunk,
/// one bounded line and one bounded message at a time; callers drain results.
pub struct MboxrdDecoder {
priv max_message_bytes : Int
priv max_line_bytes : Int
priv line : @buffer.Buffer
priv body : @buffer.Buffer
priv mut blank : Bytes
priv mut envelope : Bytes?
priv mut offset : Int64
priv mut line_start : Int64
priv mut message_start : Int64
priv mut number : Int64
priv mut closed : Bool
priv mut failed : Bool
}
///|
/// Budgets count DECODED content bytes and physical line bytes including LF.
/// No whole-file size limit; offsets and record numbers use signed 64-bit ints.
pub fn MboxrdDecoder::new(
max_message_bytes? : Int = 33554432,
max_line_bytes? : Int = 1048576,
) -> MboxrdDecoder raise MailboxError {
if max_message_bytes < 0 ||
max_message_bytes > 33554432 ||
max_line_bytes < 6 ||
max_line_bytes > 1048576 {
raise Invalid("mboxrd budgets: message 0..32MiB, physical line 6..1MiB")
}
{
max_message_bytes,
max_line_bytes,
line: @buffer.Buffer(),
body: @buffer.Buffer(),
blank: b"",
envelope: None,
offset: 0L,
line_start: 0L,
message_start: 0L,
number: 0L,
closed: false,
failed: false,
}
}
///|
fn MboxrdDecoder::append_body(
self : MboxrdDecoder,
data : BytesView,
) -> Unit raise MailboxError {
if data.length() > self.max_message_bytes - self.body.length() {
raise Invalid("mboxrd message byte budget exceeded")
}
self.body.write_bytes(data)
}
///|
fn MboxrdDecoder::complete(
self : MboxrdDecoder,
end : Int64,
) -> RawMessage raise MailboxError {
if self.blank.is_empty() {
raise Invalid("mboxrd message missing final container blank line")
}
let envelope = match self.envelope {
Some(bytes) => bytes
None => raise Invalid("mboxrd message missing envelope")
}
// A pending final blank is the CONTAINER separator, not part of the content.
let message = RawMessage::{
number: self.number,
source_start: self.message_start,
source_end: end,
envelope,
content: self.body.to_bytes(),
}
self.body.reset()
self.blank = b""
message
}
///|
fn MboxrdDecoder::accept_line(
self : MboxrdDecoder,
out : Array[RawMessage],
) -> Unit raise MailboxError {
let bytes = self.line.to_bytes()
self.line.reset()
let end = if bytes.length() >= 2 && bytes[bytes.length() - 2] == b'\r' {
bytes.length() - 2
} else {
bytes.length() - 1
}
if bytes.has_prefix(b"From ") {
if self.envelope is Some(_) {
out.push(self.complete(self.line_start))
}
if end <= 5 {
raise Invalid("mboxrd empty envelope")
}
let mut nonblank = false
for i in 5..' {
quoted += 1
}
let skip = if quoted > 0 && bytes[quoted:].has_prefix(b"From ") {
1
} else {
0
}
self.append_body(bytes[skip:])
}
}
self.line_start = self.offset
}
///|
fn MboxrdDecoder::poison(self : MboxrdDecoder) -> Unit {
self.failed = true
self.line.reset()
self.body.reset()
self.envelope = None
self.blank = b""
}
///|
/// Results are complete records only. Any error poisons the entire decoder;
/// previously returned records stay valid, but this call returns no partial array.
pub fn MboxrdDecoder::feed(
self : MboxrdDecoder,
chunk : Bytes,
) -> Array[RawMessage] raise MailboxError {
if self.failed || self.closed {
raise Invalid("mboxrd decoder is failed or finished")
}
errdefer self.poison()
if chunk.length() > 1048576 {
raise Invalid("mboxrd feed chunk exceeds 1MiB")
}
if self.offset > 9223372036854775807L - chunk.length().to_int64() {
raise Invalid("mboxrd byte offset exhausted")
}
let out : Array[RawMessage] = []
for byte in chunk {
if self.line.length() == self.max_line_bytes {
raise Invalid("mboxrd physical line byte budget exceeded")
}
self.line.write_byte(byte)
self.offset += 1L
if byte == b'\n' {
self.accept_line(out)
}
}
out
}
///|
/// Strict EOF: a nonempty partial line or a missing container blank is truncated.
/// The empty mailbox returns [], and successful completion closes the decoder.
pub fn MboxrdDecoder::finish(
self : MboxrdDecoder,
) -> Array[RawMessage] raise MailboxError {
if self.failed || self.closed {
raise Invalid("mboxrd decoder is failed or finished")
}
errdefer self.poison()
if !self.line.is_empty() {
raise Invalid("mboxrd truncated final physical line")
}
let result = match self.envelope {
None => []
Some(_) => [self.complete(self.offset)]
}
self.closed = true
self.envelope = None
result
}