//// Encodes quoted-printable strings according to RFC 2045 //// and RFC 2047. //// //// See the following links for reference: //// - //// - 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 { <> -> do_encode_bytes(other_bytes, [ "=" <> bit_array.base16_encode(byte), ..accumulator ]) <<_:bits>> -> accumulator |> list.reverse() |> string.join("") } }