///|
fn percent_bytes(text : String, encoded : Bool) -> Bytes raise MailboxError {
let input = @utf8.encode(text)
let out : Array[Byte] = []
let mut at = 0
while at < input.length() {
if input[at] == 37 && encoded {
if at + 2 >= input.length() {
raise Invalid("truncated MIME percent escape")
}
out.push(
(mime_hex(input[at + 1]) * 16 + mime_hex(input[at + 2])).to_byte(),
)
at += 3
} else {
out.push(input[at])
at += 1
}
}
Bytes::from_array(out)
}
///|
fn resolve_parameters(
raw : Map[String, String],
) -> Map[String, String] raise MailboxError {
let result = Map([])
let groups : Map[String, Map[Int, (Bool, String)]] = Map([])
let singles : Map[String, Bool] = Map([])
if raw.length() > 256 {
raise Invalid("MIME parameter count limit")
}
for key, value in raw {
match key.find("*") {
None => result[key] = value
Some(pos) => {
let name = key[:pos].to_owned()
let tail = key[pos + 1:].to_owned()
if name.is_empty() {
raise Invalid("empty extended parameter name")
}
let encoded = tail.is_empty() || tail.has_suffix("*")
let index = if tail.is_empty() {
singles[name] = true
0
} else {
let digits = if encoded {
tail[:tail.length() - 1].to_owned()
} else {
tail
}
if digits.is_empty() ||
digits.length() > 3 ||
(digits.length() > 1 && digits.has_prefix("0")) {
raise Invalid("invalid MIME continuation index")
}
let mut number = 0
for c in digits.iter() {
if c < '0' || c > '9' {
raise Invalid("invalid MIME continuation index")
}
number = number * 10 + c.to_int() - 48
}
if number > 255 {
raise Invalid("MIME continuation index limit")
}
number
}
let group = match groups.get(name) {
Some(g) => g
None => {
let g = Map([])
groups[name] = g
g
}
}
if group.contains(index) {
raise Invalid("duplicate MIME continuation")
}
group[index] = (encoded, value)
}
}
}
for name, group in groups {
if result.contains(name) || (singles.contains(name) && group.length() != 1) {
raise Invalid("ambiguous MIME parameter alternatives")
}
let bytes : Array[Byte] = []
let mut charset = "us-ascii"
let mut first_encoded = false
for i in 0.. v
None => raise Invalid("gap in MIME continuation")
}
let mut value = raw_value
if i == 0 && encoded {
first_encoded = true
let pieces = value.split("'").map(x => x.to_owned()).collect()
if pieces.length() < 3 {
raise Invalid("missing MIME charset/language delimiters")
}
charset = if pieces[0].is_empty() { "us-ascii" } else { pieces[0] }
value = pieces[2:].to_owned().join("'")
}
for byte in percent_bytes(value, encoded) {
bytes.push(byte)
}
}
result[name] = if first_encoded {
decode_charset(Bytes::from_array(bytes), charset)
} else {
@utf8.decode(Bytes::from_array(bytes)) catch {
_ => raise Invalid("invalid unlabelled MIME parameter")
}
}
}
result
}
///|
/// Returns display metadata only; never use the result directly as a filesystem path.
pub fn MimePart::filename(self : MimePart) -> String? raise MailboxError {
let values = self.fields.filter(h => {
h.name.to_lower() == "content-disposition"
})
if values.length() > 1 {
raise Invalid("duplicate Content-Disposition")
}
if !values.is_empty() {
let (_, params) = parse_mime_parameters(values[0].value)
if params.get("filename") is Some(value) {
return Some(value)
}
}
self.parameters.get("name")
}