///|
/// A `field-name` is any printable ASCII character except `:` (RFC 5322 3.6.1,
/// `ftext`). MoonMail additionally requires a nonempty name.
pub fn is_valid_field_name(name : String) -> Bool {
if name.is_empty() {
return false
}
for ch in name {
let c = ch.to_int()
if !((c >= 33 && c <= 57) || (c >= 59 && c <= 126)) {
return false
}
}
true
}
///|
/// Check a header value for injection: CR, LF and NUL are all rejected
/// (RFC 5322 3.6.8 / security model). See `injection.mbt`.
pub fn check_header_value(
value : String,
context : String,
) -> Unit raise MailFailure {
reject_crlf(value, context)
}
///|
/// Build a single (possibly folded) header field `Name: value` followed by
/// CRLF. The value is validated against CR/LF injection and folded so that no
/// line exceeds `max_width` (default 78) columns, per RFC 5322 2.2.3.
pub fn make_header(
name : String,
value : String,
max_width? : Int = 78,
) -> String raise MailFailure {
guard is_valid_field_name(name) else {
raise MailFailure::of(
InvalidHeader,
MM_HEADER_004,
"invalid header field name: \{name}",
)
}
reject_crlf(value, "header '\{name}'")
fold_header_value(name, value, max_width)
}
///|
/// Fold `value` after the `Name: ` prefix so every line is <= `max_width`.
/// Continuation lines start with a single space (RFC 5322 folding whitespace).
fn fold_header_value(name : String, value : String, max_width : Int) -> String {
let prefix = "\{name}: "
let text = prefix + value
let bytes = string_to_bytes(text)
let out = FixedArray::make(bytes.length() + 32, b'\x00')
let mut j = 0
let mut i = 0
let mut col = 0
while i < bytes.length() {
let remaining = bytes.length() - i
if col + remaining <= max_width {
for k = i; k < bytes.length(); k = k + 1 {
out[j] = bytes[k]
j += 1
}
break
}
let window = max_width - col
if window < 1 {
break
}
let last_space = find_last_space(bytes, i, i + window)
if last_space > i {
for k = i; k < last_space; k = k + 1 {
out[j] = bytes[k]
j += 1
}
out[j] = b'\r'
out[j + 1] = b'\n'
out[j + 2] = b' '
j += 3
i = last_space + 1
col = 1
} else {
// no whitespace to break at: hard break inside the token
for k = i; k < i + window; k = k + 1 {
out[j] = bytes[k]
j += 1
}
out[j] = b'\r'
out[j + 1] = b'\n'
out[j + 2] = b' '
j += 3
i = i + window
col = 1
}
}
fixedarray_to_string(out, j) + "\r\n"
}
///|
/// Find the index of the last space within `bytes[start:end]`, or `-1`.
fn find_last_space(bytes : Bytes, start : Int, end : Int) -> Int {
let mut idx = -1
for j = start; j < end; j = j + 1 {
if bytes[j] == b' ' {
idx = j
}
}
idx
}
///|
/// Remove folding whitespace: each CRLF followed by WSP is removed and the
/// WSP collapsed. Used by tests and by MIME parsers.
pub fn unfold_header_value(value : String) -> String {
let bytes = string_to_bytes(value)
let out = FixedArray::make(bytes.length(), b'\x00')
let mut j = 0
let mut i = 0
while i < bytes.length() {
if bytes[i] == b'\r' && i + 1 < bytes.length() && bytes[i + 1] == b'\n' {
let mut k = i + 2
while k < bytes.length() && (bytes[k] == b' ' || bytes[k] == b'\t') {
k += 1
}
if k > i + 2 {
// collapsed folding whitespace: emit one space
out[j] = b' '
j += 1
}
i = k
} else {
out[j] = bytes[i]
j += 1
i += 1
}
}
fixedarray_to_string(out, j)
}