///|
/// True when the string contains a CR, LF or bare control newline, which must
/// never appear inside a header field value.
pub fn contains_crlf(s : String) -> Bool {
  for b in string_to_bytes(s) {
    if b == b'\r' || b == b'\n' {
      return true
    }
  }
  false
}

///|
/// Reject any CR, LF or NUL byte in `value`. This is the core header-injection
/// guard (MM_INJ_001 / MM_INJ_002): user-controlled input must not introduce
/// new header fields or terminate the header block.
pub fn reject_crlf(value : String, context : String) -> Unit raise MailFailure {
  for b in string_to_bytes(value) {
    if b == b'\r' || b == b'\n' {
      raise MailFailure::of(HeaderInjection, MM_INJ_001, "CR/LF in \{context}")
    }
    if b == b'\x00' {
      raise MailFailure::of(
        HeaderInjection,
        MM_INJ_002,
        "NUL byte in \{context}",
      )
    }
  }
}

///|
/// Sanitize a filename for use inside `Content-Disposition`: strips path
/// separators, CR/LF, NUL and other control characters (MM_INJ_003).
pub fn sanitize_filename(name : String) -> String {
  let buf = Buffer::Buffer(size_hint=name.length())
  for ch in name {
    let c = ch.to_int()
    if c == 0 || c == 0x0A || c == 0x0D || c == 0x2F || c == 0x5C {
      continue
    }
    if c < 0x20 || c == 0x7F {
      buf.write_char_utf16le('_')
    } else {
      buf.write_char_utf16le(ch)
    }
  }
  let s = buf.to_string().trim().to_owned()
  if s.is_empty() {
    "attachment"
  } else {
    s
  }
}