///|
/// A parsed MIME Content-Type: a type/subtype plus ordered parameters
/// (RFC 2045 section 5.1).
pub struct ContentType {
type_ : String
subtype : String
params : Array[(String, String)]
} derive(Eq, Debug)
///|
pub fn ContentType::new(type_ : String, subtype : String) -> ContentType {
{ type_, subtype, params: [] }
}
///|
pub fn ContentType::type_name(self : ContentType) -> String {
self.type_
}
///|
pub fn ContentType::subtype_name(self : ContentType) -> String {
self.subtype
}
///|
pub fn ContentType::params(self : ContentType) -> Array[(String, String)] {
self.params
}
///|
/// Add (or replace) a parameter like `boundary=...` or `charset=utf-8`.
pub fn ContentType::set_param(
self : ContentType,
name : String,
value : String,
) -> Unit {
for i = 0; i < self.params.length(); i = i + 1 {
let (n, _) = self.params[i]
if n.to_lower() == name.to_lower() {
self.params[i] = (name, value)
return
}
}
self.params.push((name, value))
}
///|
pub fn ContentType::get_param(self : ContentType, name : String) -> String? {
for pair in self.params {
let (n, v) = pair
if n.to_lower() == name.to_lower() {
return Some(v)
}
}
None
}
///|
pub fn ContentType::is_multipart(self : ContentType) -> Bool {
self.type_.to_lower() == "multipart"
}
///|
/// Validate a token used for the type, subtype and parameter names
/// (RFC 2045 section 5.1: any printable ASCII except tspecials).
fn is_token(s : String) -> Bool {
if s.is_empty() {
return false
}
for b in string_to_bytes(s) {
let c = b.to_int()
let special = c == 0x28 ||
c == 0x29 ||
c == 0x2C ||
c == 0x2F ||
c == 0x3A ||
c == 0x3B ||
c == 0x3C ||
c == 0x3D ||
c == 0x3E ||
c == 0x3F ||
c == 0x40 ||
c == 0x5B ||
c == 0x5C ||
c == 0x5D ||
c == 0x22
if c < 33 || c > 126 || special {
return false
}
}
true
}
///|
/// Render the Content-Type header value, e.g.
/// `multipart/mixed; boundary="abc"`.
pub fn ContentType::to_string(self : ContentType) -> String {
let buf = Buffer::Buffer()
buf.write_string_utf16le(
"\{self.type_.to_lower()}/\{self.subtype.to_lower()}",
)
for pair in self.params {
let (name, value) = pair
buf.write_string_utf16le("; \{name.to_lower()}=")
if needs_quoted_param(value) {
buf.write_string_utf16le("\"\{value}\"")
} else {
buf.write_string_utf16le(value)
}
}
buf.to_string()
}
///|
fn needs_quoted_param(value : String) -> Bool {
for b in string_to_bytes(value) {
let c = b.to_int()
if c < 33 || c > 126 {
return true
}
}
false
}
///|
/// Parse a Content-Type header value into a `ContentType`.
pub fn ContentType::parse(input : String) -> ContentType raise MailFailure {
let s = input.trim().to_owned()
guard !s.is_empty() else {
raise MailFailure::of(Mime, MM_MIME_005, "empty content-type")
}
let semi = index_of(s, ";")
let base = if semi >= 0 { s[0:semi].trim().to_owned() } else { s }
let slash = index_of(base, "/")
guard slash > 0 else {
raise MailFailure::of(Mime, MM_MIME_005, "content-type missing '/'")
}
let type_ = base[0:slash].trim().to_owned()
let subtype = base[slash + 1:base.length()].trim().to_owned()
guard is_token(type_) && is_token(subtype) else {
raise MailFailure::of(Mime, MM_MIME_005, "invalid content-type: \{s}")
}
let ct = ContentType::new(type_, subtype)
if semi >= 0 {
parse_params(ct, s[semi + 1:s.length()].to_owned())
}
ct
}
///|
fn parse_params(ct : ContentType, rest : String) -> Unit raise MailFailure {
let items = split_semicolons(rest)
for item in items {
let item = item.trim().to_owned()
if item.is_empty() {
continue
}
let eq = index_of(item, "=")
guard eq > 0 else {
raise MailFailure::of(
Mime,
MM_MIME_005,
"malformed content-type parameter: \{item}",
)
}
let name = item[0:eq].trim().to_owned()
let value = item[eq + 1:item.length()].trim().to_owned()
let value_bytes = string_to_bytes(value)
let value = if value_bytes.length() >= 2 && value_bytes[0] == b'"' {
// strip surrounding quotes
value[1:value.length() - 1].to_owned()
} else {
value
}
ct.set_param(name, value)
}
}
///|
/// Split parameter text on `;` outside quoted strings.
fn split_semicolons(s : String) -> Array[String] {
let items = []
let bytes = string_to_bytes(s)
let mut start = 0
let mut in_quote = false
for i = 0; i < bytes.length(); i = i + 1 {
if bytes[i] == b'"' {
in_quote = !in_quote
} else if bytes[i] == b';' && !in_quote {
items.push(s[start:i].to_owned())
start = i + 1
}
}
items.push(s[start:bytes.length()].to_owned())
items
}