///|
pub struct Program {
syntax : Script
capabilities : Array[String]
limits : Limits
}
///|
priv struct Arguments {
flags : Array[String]
options : Map[String, String]
values : Array[Argument]
}
///|
pub fn supported_capabilities() -> Array[String] {
[
"relational", "comparator-i;ascii-numeric", "subaddress", "variables", "copy",
"fileinto", "envelope", "comparator-i;octet", "comparator-i;ascii-casemap",
]
}
///|
fn split_args(
args : Array[Argument],
span : Span,
) -> Arguments raise SieveError {
let flags : Array[String] = []
let options : Map[String, String] = Map([])
let values : Array[Argument] = []
let mut i = 0
let mut positional = false
while i < args.length() {
match args[i] {
Flag(name) => {
if positional {
fail(
"check.tag_order", "tags must precede positional arguments", span,
)
}
if flags.contains(name) {
fail("check.duplicate_tag", "duplicate tag :\{name}", span)
}
flags.push(name)
if name == "comparator" || name == "value" || name == "count" {
i += 1
if i >= args.length() {
fail("check.tag_value", "tag requires a string value", span)
}
match args[i] {
Literal(value) => options[name] = value
_ =>
fail("check.tag_value", "tag value must be a single string", span)
}
}
}
arg => {
positional = true
values.push(arg)
}
}
i += 1
}
{ flags, options, values }
}
///|
fn strings_arg(arg : Argument, span : Span) -> Array[String] raise SieveError {
match arg {
Literal(s) => [s]
Strings(ss) => ss
_ => {
fail("check.string", "expected string or string list", span)
[]
}
}
}
///|
fn single_arg(arg : Argument, span : Span) -> String raise SieveError {
match arg {
Literal(s) => s
_ => {
fail("check.single", "expected a single string", span)
""
}
}
}
///|
fn count_args(a : Arguments, n : Int, span : Span) -> Unit raise SieveError {
if a.values.length() != n {
fail("check.arity", "expected \{n} positional arguments", span)
}
}
///|
fn allowed_flags(
a : Arguments,
allowed : Array[String],
span : Span,
) -> Unit raise SieveError {
for flag in a.flags {
if !allowed.contains(flag) {
fail("check.tag", "unsupported tag :\{flag}", span)
}
}
}
///|
fn exclusive(
a : Arguments,
group : Array[String],
span : Span,
) -> Unit raise SieveError {
let mut count = 0
for flag in group {
if a.flags.contains(flag) {
count += 1
}
}
if count > 1 {
fail("check.conflicting_tags", "mutually exclusive tags", span)
}
}
///|
fn needs(
caps : Array[String],
name : String,
span : Span,
) -> Unit raise SieveError {
if !caps.contains(name) {
fail("check.require", "missing require \"\{name}\"", span)
}
}
///|
fn check_match(a : Arguments, span : Span) -> Unit raise SieveError {
exclusive(a, ["is", "contains", "matches", "value", "count"], span)
match a.options.get("comparator") {
Some("i;octet" | "i;ascii-casemap" | "i;ascii-numeric") | None => ()
_ => fail("check.comparator", "unsupported comparator", span)
}
}
///|
fn check_test(expr : Test, caps : Array[String]) -> Unit raise SieveError {
match expr {
Not(child, _) => check_test(child, caps)
AnyOf(children, _) | AllOf(children, _) =>
for child in children {
check_test(child, caps)
}
Call(name, args, span) => {
let a = split_args(args, span)
match name {
"true" | "false" => {
allowed_flags(a, [], span)
count_args(a, 0, span)
}
"exists" => {
allowed_flags(a, [], span)
count_args(a, 1, span)
ignore(strings_arg(a.values[0], span))
}
"size" => {
allowed_flags(a, ["over", "under"], span)
count_args(a, 1, span)
if a.flags.length() != 1 {
fail("check.size", "size requires :over or :under", span)
}
match a.values[0] {
Quantity(_) => ()
_ => fail("check.number", "size requires a number", span)
}
}
"string" => {
needs(caps, "variables", span)
allowed_flags(
a,
["is", "contains", "matches", "comparator", "value", "count"],
span,
)
count_args(a, 2, span)
check_match(a, span)
check_relational(a, caps, span)
ignore(strings_arg(a.values[0], span))
ignore(strings_arg(a.values[1], span))
}
"header" | "address" | "envelope" => {
let flags = [
"is", "contains", "matches", "comparator", "value", "count",
]
if name != "header" {
flags.push("user")
flags.push("detail")
if a.flags.contains("user") || a.flags.contains("detail") {
needs(caps, "subaddress", span)
}
flags.push("all")
flags.push("localpart")
flags.push("domain")
exclusive(a, ["all", "localpart", "domain", "user", "detail"], span)
}
if name == "envelope" {
needs(caps, "envelope", span)
}
allowed_flags(a, flags, span)
count_args(a, 2, span)
check_match(a, span)
check_relational(a, caps, span)
let fields = strings_arg(a.values[0], span)
ignore(strings_arg(a.values[1], span))
if name == "envelope" {
for field in fields {
if ascii_lower(field) != "from" && ascii_lower(field) != "to" {
fail(
"check.envelope", "only from/to envelope fields are supported",
span,
)
}
}
}
}
_ => fail("check.test", "unknown or unsupported test: \{name}", span)
}
}
}
}
///|
fn check_statements(
body : Array[Statement],
caps : Array[String],
top : Bool,
) -> Unit raise SieveError {
let mut executable = false
for statement in body {
match statement {
Command(name, args, span) => {
let a = split_args(args, span)
if name == "set" {
check_set(a, caps, span)
} else if name == "fileinto" || name == "redirect" {
allowed_flags(a, ["copy"], span)
if a.flags.contains("copy") {
needs(caps, "copy", span)
}
} else {
allowed_flags(a, [], span)
}
match name {
"require" => {
if !top || executable {
fail(
"check.require_order", "require must precede executable commands at top level",
span,
)
}
count_args(a, 1, span)
for cap in strings_arg(a.values[0], span) {
if !supported_capabilities().contains(cap) {
fail("check.capability", "unsupported capability: \{cap}", span)
}
if !caps.contains(cap) {
caps.push(cap)
}
}
}
"set" => executable = true
"keep" | "discard" | "stop" => {
executable = true
count_args(a, 0, span)
}
"fileinto" | "redirect" => {
executable = true
count_args(a, 1, span)
let destination = single_arg(a.values[0], span)
if destination.is_empty() ||
destination.contains_char('\r') ||
destination.contains_char('\n') {
fail(
"check.destination", "destination must be nonempty and single-line",
span,
)
}
if name == "fileinto" {
needs(caps, "fileinto", span)
}
}
_ =>
fail(
"check.command",
"unknown or unsupported command: \{name}",
span,
)
}
}
Branch(branches, otherwise, _) => {
executable = true
for branch in branches {
check_test(branch.condition, caps)
check_statements(branch.body, caps, false)
}
check_statements(otherwise, caps, false)
}
}
}
}
///|
pub fn compile(
source : String,
limits? : Limits = Limits::default(),
) -> Program raise SieveError {
let syntax = parse(source, limits~)
let capabilities : Array[String] = []
check_statements(syntax.statements, capabilities, true)
{ syntax, capabilities, limits }
}
///|
pub fn Program::required_capabilities(self : Program) -> Array[String] {
self.capabilities.copy()
}