///|
pub(all) struct Header {
name : String
value : String
} derive(Eq, Debug)
///|
pub struct Message {
headers : Array[Header]
size : Int
envelope_from : String
envelope_to : Array[String]
}
///|
pub(all) struct Mailbox {
address : String
localpart : String
domain : String
} derive(Eq, Debug)
///|
fn valid_header_name(name : String) -> Bool {
!name.is_empty() && name.all(c => c.is_ascii_graphic() && c != ':')
}
///|
fn single_line(value : String) -> Bool {
!value.contains_char('\r') &&
!value.contains_char('\n') &&
!value.contains_char('\u{0}')
}
///|
pub fn Message::new(
headers : Array[Header],
size : Int,
envelope_from? : String = "",
envelope_to? : Array[String] = [],
limits? : Limits = Limits::default(),
) -> Message raise SieveError {
limits.check()
if size < 0 {
fail("message.size", "message size must be nonnegative", origin())
}
if headers.length() > limits.headers {
fail("limit.headers", "too many headers", origin())
}
let mut total = 0
for header in headers {
if header.name.length() > limits.string_chars ||
header.value.length() > limits.string_chars {
fail("limit.string", "header name or value too long", origin())
}
if header.name.length() > limits.source_chars - total {
fail("limit.message", "normalized message exceeds limit", origin())
}
total += header.name.length()
if header.value.length() > limits.source_chars - total {
fail("limit.message", "normalized message exceeds limit", origin())
}
total += header.value.length()
if !valid_header_name(header.name) {
fail("message.header_name", "invalid header field name", origin())
}
if !single_line(header.value) {
fail(
"message.header_value",
"provide unfolded, NUL-free header values",
origin(),
)
}
}
if envelope_to.length() > 1 {
fail(
"message.recipient",
"Sieve runs for one recipient; split recipient simulations",
origin(),
)
}
if envelope_to.any(address => address.is_empty()) {
fail(
"message.recipient",
"recipient cannot be an empty reverse path",
origin(),
)
}
for address in [envelope_from, ..envelope_to] {
if address.length() > limits.string_chars {
fail("limit.string", "envelope address too long", origin())
}
if address.length() > limits.source_chars - total {
fail(
"limit.message",
"normalized message and envelope exceed limit",
origin(),
)
}
total += address.length()
if !address.is_empty() && parse_mailbox(address) is None {
fail(
"message.envelope",
"envelope must contain bare supported addr-spec values",
origin(),
)
}
}
{
headers: headers.copy(),
size,
envelope_from,
envelope_to: envelope_to.copy(),
}
}
///|
pub fn Message::header_values(self : Message, name : String) -> Array[String] {
let key = ascii_lower(name)
self.headers.filter_map(h => {
if ascii_lower(h.name) == key {
Some(h.value)
} else {
None
}
})
}
///|
pub fn Message::octet_size(self : Message) -> Int {
self.size
}
///|
pub fn parse_mailbox(value : String) -> Mailbox? {
let source = value.trim().to_owned()
if source.is_empty() || source.length() > 65536 || !single_line(source) {
return None
}
let chars = source.to_array()
let mut quoted = false
let mut escaped = false
let mut at = -1
for i = 0; i < chars.length(); i = i + 1 {
let c = chars[i]
if escaped {
escaped = false
continue
}
if quoted && c == '\\' {
escaped = true
continue
}
if c == '"' {
quoted = !quoted
continue
}
if !quoted && c == '@' {
if at >= 0 {
return None
}
at = i
}
}
if quoted || escaped || at <= 0 || at == chars.length() - 1 {
return None
}
let local_raw = chars_text(chars, 0, at)
let domain = ascii_lower(chars_text(chars, at + 1, chars.length()))
let localpart = if chars[0] == '"' {
if at < 2 || chars[at - 1] != '"' {
return None
}
let out = StringBuilder()
let mut i = 1
while i < at - 1 {
if chars[i] == '\\' {
i += 1
if i >= at - 1 {
return None
}
} else if chars[i] == '"' {
return None
}
out.write_char(chars[i])
i += 1
}
out.to_string()
} else {
if local_raw.has_prefix(".") ||
local_raw.has_suffix(".") ||
local_raw.contains("..") {
return None
}
if !local_raw.all(c => {
(c.is_ascii_alphabetic() || c.is_ascii_digit()) ||
"!#$%&'*+-/=?^_\u{60}{|}~.".contains_char(c)
}) {
return None
}
local_raw
}
if domain.has_prefix("[") && domain.has_suffix("]") {
if domain.length() < 3 || !domain.all(c => c.is_ascii_graphic() && c != '@') {
return None
}
} else {
if domain.is_empty() ||
domain.has_prefix(".") ||
domain.has_suffix(".") ||
domain.contains("..") {
return None
}
for label in domain.split(".") {
let s = label.to_owned()
if s.is_empty() ||
s.has_prefix("-") ||
s.has_suffix("-") ||
!s.all(c => (c.is_ascii_alphabetic() || c.is_ascii_digit()) || c == '-') {
return None
}
}
}
Some({ address: source, localpart, domain })
}
///|
fn mailbox_piece(piece : String) -> Mailbox? {
let value = piece.trim().to_owned()
match value.split_once("<") {
Some((_, rest)) =>
match rest.to_owned().split_once(">") {
Some((address, tail)) if tail.to_owned().trim().is_empty() =>
parse_mailbox(address.to_owned())
_ => None
}
None => parse_mailbox(value)
}
}
///|
pub fn extract_mailboxes(
value : String,
limits? : Limits = Limits::default(),
) -> Array[Mailbox] raise SieveError {
limits.check()
if value.length() > limits.string_chars {
fail("limit.string", "address header too long", origin())
}
let out : Array[Mailbox] = []
let piece = StringBuilder()
let mut quoted = false
let mut escaped = false
let mut angle = false
let mut comment = 0
for c in value {
if escaped {
if comment == 0 {
piece.write_char(c)
}
escaped = false
continue
}
if (quoted || comment > 0) && c == '\\' {
if comment == 0 {
piece.write_char(c)
}
escaped = true
continue
}
if comment > 0 {
if c == '(' {
comment += 1
}
if c == ')' {
comment -= 1
}
if comment > limits.depth {
fail("limit.depth", "address comment nesting too deep", origin())
}
continue
}
if c == '"' {
quoted = !quoted
piece.write_char(c)
continue
}
if !quoted {
if c == '(' {
comment = 1
continue
}
if c == '<' {
angle = true
}
if c == '>' {
angle = false
}
if !angle && c == ':' {
piece.reset()
continue
}
if !angle && (c == ',' || c == ';') {
match mailbox_piece(piece.to_string()) {
Some(m) => out.push(m)
None => ()
}
piece.reset()
if out.length() > limits.headers {
fail("limit.addresses", "too many addresses", origin())
}
continue
}
}
piece.write_char(c)
}
if !quoted && comment == 0 && !angle && !escaped {
match mailbox_piece(piece.to_string()) {
Some(m) => out.push(m)
None => ()
}
}
if out.length() > limits.headers {
fail("limit.addresses", "too many addresses", origin())
}
out
}