Skip to main content

src/internal/encoder.gleam

///// Email content encoding utilities for MIME messages.
///// This module provides functions for encoding email addresses and strings
///// using various MIME content transfer encodings such as Base64, Quoted-Printable,
///// and others defined in RFC standards.

import gleam/int
import gleam/list
import gleam/pair
import gleam/result

import internal/encoder/base64 as b64
import internal/encoder/bit
import internal/encoder/encoding.{
  type Encoding, Base64, Bit, PercentEncoding, QuotedPrintable, Text,
}
import internal/encoder/idna
import internal/encoder/percent_encoding as pe
import internal/encoder/quoted_printable as qp
import internal/encoder/text

/// Encodes an email address using IDNA (Internationalized Domain Names in Applications).
pub fn encode_email_address(
  email address: String,
  with mode: encoding.EncodingMode,
) -> Result(String, encoding.EncoderError) {
  idna.encode_email_address(address, mode)
}

/// Encodes a string using the specified content transfer encoding.
///
/// This function delegates to the appropriate encoder based on the encoding type.
pub fn encode_string(
  encode string: String,
  using encoding: Encoding,
  with mode: encoding.EncodingMode,
  start position: Int,
  preferred_size preferred_size: Int,
  maximum_size maximum_size: Int,
) -> Result(List(String), encoding.EncoderError) {
  case encoding {
    Base64 -> b64.encode_string(string, position, preferred_size)
    Bit(bits) -> bit.encode_string(string, bits, mode, maximum_size)
    PercentEncoding -> pe.encode_string(string, position, preferred_size)
    QuotedPrintable(rfc) ->
      qp.encode_string(string, rfc, position, preferred_size)
    Text(field_type) ->
      text.encode_string(
        string,
        field_type,
        mode,
        position,
        preferred_size,
        maximum_size,
      )
  }
}

/// Determines the optimal order of encodings to try for encoding a string within a size limit.
///
/// Estimates the encoded size for each encoding and returns them sorted by size
/// from smallest to largest, filtering out encodings that would exceed the maximum size.
/// This is useful for content negotiation to find the most efficient encoding.
pub fn determine_encoding_order(
  order encodings: List(Encoding),
  encode string: String,
  with mode: encoding.EncodingMode,
  maximum_size maximum_size: Int,
) -> List(Encoding) {
  encodings
  |> list.map(fn(encoding) {
    encoding
    |> estimate_encoded_size(mode, string, maximum_size)
    |> result.map(fn(size) { #(encoding, size) })
  })
  |> result.values()
  |> list.sort(fn(a, b) { int.compare(pair.second(a), pair.second(b)) })
  |> list.map(pair.first)
}

/// Returns the canonical string name for an encoding type.
pub fn name(encoding: Encoding) -> String {
  case encoding {
    Base64 -> "base64"
    Bit(bits) -> int.to_string(bits) <> "bit"
    PercentEncoding -> "percent-encoding"
    QuotedPrintable(_) -> "quoted-printable"
    Text(_) -> "quoted-string"
  }
}

fn estimate_encoded_size(
  encoding: Encoding,
  mode: encoding.EncodingMode,
  value: String,
  maximum_size: Int,
) -> Result(Int, encoding.EncoderError) {
  case encoding {
    Base64 -> b64.estimate_encoded_size(value)
    Bit(bits) -> bit.estimate_encoded_size(value, bits, mode, maximum_size)
    PercentEncoding -> pe.estimate_encoded_size(value, maximum_size)
    QuotedPrintable(rfc) -> qp.estimate_encoded_size(value, rfc)
    Text(field_type) ->
      text.estimate_encoded_size(value, field_type, mode, maximum_size)
  }
}