//// Encodes quoted-printable strings according to RFC 2045
//// and RFC 2047.
////
//// See the following links for reference:
//// - <https://tools.ietf.org/html/rfc2045#section-6.7>
//// - <https://tools.ietf.org/html/rfc2047>
import gleam/bit_array
import gleam/bool
import gleam/list
import gleam/order
import gleam/result
import gleam/string
import internal/encoder/encoding.{type Rfc, Rfc2047}
/// Estimates the quoted printable encoded size of a string in bytes,
/// taking the desired RFC constraints into account.
pub fn estimate_encoded_size(
of string: String,
enforce rfc: Rfc,
) -> Result(Int, encoding.EncoderError) {
string
|> string.split("")
|> list.fold(0, fn(size, character) {
let character_size = string.byte_size(character)
case should_encode_character(character, rfc) {
True -> size + { 3 * character_size }
False -> size + character_size
}
})
|> Ok
}
/// Quoted printable encode a string, taking the desired RFC
/// constraints and the preferred line size into account, and
/// pretending to start the first line at the passed start position.
pub fn encode_string(
encode string: String,
enforce rfc: Rfc,
start position: Int,
preferred_size preferred_size: Int,
) -> Result(List(String), encoding.EncoderError) {
string
|> string.split("")
|> do_encode_string(rfc, preferred_size, #(position, []))
|> result.map(list.reverse)
}
fn do_encode_string(
characters: List(String),
rfc: Rfc,
preferred_size: Int,
accumulator: #(Int, List(String)),
) -> Result(List(String), encoding.EncoderError) {
let #(used_size, lines) = accumulator
case characters {
[] -> Ok(lines)
[character, ..other_characters] -> {
let force_encoding =
is_whitespace(character) && list.is_empty(other_characters)
let encoded_character = encode_character(character, rfc, force_encoding)
let encoded_character_size = string.byte_size(encoded_character)
case lines {
[] -> #(encoded_character_size, [encoded_character])
[first_line, ..other_lines]
if { used_size + encoded_character_size < preferred_size }
|| {
other_characters == []
&& used_size + encoded_character_size == preferred_size
}
-> #(used_size + encoded_character_size, [
first_line <> encoded_character,
..other_lines
])
[first_line, ..other_lines] -> #(encoded_character_size, [
encoded_character,
first_line <> "=",
..other_lines
])
}
|> do_encode_string(other_characters, rfc, preferred_size, _)
}
}
}
fn should_encode_character(character: String, rfc: Rfc) -> Bool {
character != "\t"
&& {
string.compare(character, "\u{20}") == order.Lt
|| string.compare(character, "\u{7f}") == order.Gt
|| string.compare(character, "=") == order.Eq
|| { rfc == Rfc2047 && string.contains("()<>@,;:\"/[]?._", character) }
}
}
fn is_whitespace(character: String) -> Bool {
string.compare(character, "\t") == order.Eq
|| string.compare(character, " ") == order.Eq
}
fn encode_character(
character: String,
rfc: Rfc,
force_encoding: Bool,
) -> String {
let should_encode = should_encode_character(character, rfc) || force_encoding
use <- bool.guard(when: !should_encode, return: character)
do_encode_bytes(bit_array.from_string(character), [])
}
fn do_encode_bytes(bytes: BitArray, accumulator: List(String)) -> String {
case bytes {
<<byte:bytes-size(1), other_bytes:bytes>> ->
do_encode_bytes(other_bytes, [
"=" <> bit_array.base16_encode(byte),
..accumulator
])
<<_:bits>> -> accumulator |> list.reverse() |> string.join("")
}
}