///|
/// Quoted-Printable encoding (RFC 2045 section 6.7), implemented from scratch.
///
/// Rules applied:
/// - Tab and space must be encoded when at the end of a line.
/// - `=` must always be encoded as `=3D`.
/// - Printable ASCII 33..126 except `=` is passed through.
/// - All other bytes (control and >127) are encoded as `=XX`.
/// - Lines are limited to 76 characters including any trailing soft break
/// (MM_ENCOD_001); a soft break is a trailing `=`.
/// - Line endings are CRLF.
pub fn qp_encode(input : String) -> String {
let src = normalize_crlf(input)
let bytes = string_to_bytes(src)
let out = FixedArray::make(bytes.length() * 4 + 8, b'\x00')
let mut j = 0
let mut line_col = 0
let mut i = 0
while i < bytes.length() {
let b = bytes[i]
if b == b'\r' {
// line terminator: emit CRLF, reset column.
out[j] = b'\r'
out[j + 1] = b'\n'
j += 2
line_col = 0
i += 2
continue
}
let encoded = needs_encoding(bytes, i)
// if the encoded form does not fit in 76 columns (reserving one character
// for a possible trailing `=` soft break), emit a soft break
let token_len = if encoded { 3 } else { 1 }
if line_col + token_len > 75 {
out[j] = b'='
out[j + 1] = b'\r'
out[j + 2] = b'\n'
j += 3
line_col = 0
}
if encoded {
let v = b.to_int()
out[j] = b'='
out[j + 1] = to_hex_upper(v >> 4)
out[j + 2] = to_hex_upper(v & 0xf)
j += 3
line_col += 3
} else {
out[j] = b
j += 1
line_col += 1
}
i += 1
}
bytes_to_string(Bytes::from_iter(out.iter())[0:j].to_owned())
}
///|
/// A byte must be encoded when it is `=`, a control byte (<33), a byte >=127,
/// or a space/tab that sits at the end of a line (before CRLF or EOF).
fn needs_encoding(bytes : Bytes, i : Int) -> Bool {
let b = bytes[i]
if b == b'=' {
return true
}
let c = b.to_int()
if c < 33 || c > 126 {
return true
}
if (b == b' ' || b == b'\t') &&
(i + 1 >= bytes.length() || bytes[i + 1] == b'\r') {
return true
}
false
}
///|
/// Decode Quoted-Printable text back to a string. Soft line breaks (`=\r\n`)
/// are removed; `=XX` escapes are decoded. Raises on malformed input.
pub fn qp_decode(input : String) -> String raise MailFailure {
let bytes = string_to_bytes(input)
let out = FixedArray::make(bytes.length(), b'\x00')
let mut j = 0
let mut i = 0
while i < bytes.length() {
let b = bytes[i]
if b == b'=' {
guard i + 2 < bytes.length() else {
raise MailFailure::encode("dangling '=' in quoted-printable")
}
if bytes[i + 1] == b'\r' && bytes[i + 2] == b'\n' {
// soft line break: skip
i += 3
continue
}
if bytes[i + 1] == b'\n' {
i += 2
continue
}
let hi = hex_nibble(bytes[i + 1])
let lo = hex_nibble(bytes[i + 2])
out[j] = ((hi << 4) | lo).to_byte()
j += 1
i += 3
} else if b == b'\r' && i + 1 < bytes.length() && bytes[i + 1] == b'\n' {
out[j] = b'\r'
out[j + 1] = b'\n'
j += 2
i += 2
} else {
out[j] = b
j += 1
i += 1
}
}
bytes_to_string(Bytes::from_iter(out.iter())[0:j].to_owned())
}
///|
fn hex_nibble(b : Byte) -> Int raise MailFailure {
let c = b.to_int()
if c >= 0x30 && c <= 0x39 {
return c - 0x30
}
if c >= 0x41 && c <= 0x46 {
return c - 0x41 + 10
}
if c >= 0x61 && c <= 0x66 {
return c - 0x61 + 10
}
raise MailFailure::encode("invalid hex in quoted-printable")
}