Rename the identifiers that carried the upstream name
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m53s

Everything clients, users and operators meet now carries the fork's name,
with no aliases (SPEC.md §2.4, changed here from "protocol identifiers
stay"):

- JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside
  the fork's own urn:inbuxa:jmap.
- WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once.
- Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these
  into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in;
  a unit test fails if Cargo.lock ever moves past the vendored copy. The
  trusted runtime now names itself too, rather than answering sieve-rs's
  default.
- The web interface's OAuth client is inbuxa-webui. On every start the old
  stalwart-webui client is removed and any application naming it is moved
  over.
- The spam filter's blobs are INBUXA_SPAM_*; every start moves any left
  under the old keys, so a trained model survives.
- SQL stores and log files default to inbuxa, in the code and in the
  schema served to the admin (checksum regenerated).
- Settings are INBUXA_* only. A STALWART_* variable that's set where its
  INBUXA_* one isn't stops the server at startup, naming it.
- The version-upgrade messages link docs.inbuxa.org's migration page, and
  the OpenAPI description, smtp crate metadata and web-push test fixtures
  lose the name.

Kept on purpose, allowlisted with reasons: the OAuth key-derivation
contexts (renaming them would end every session and invalidate every
sealed client id) and the hashed application prefix.

Also fixes a latent start-up failure: ensure_client updated an existing
first-party client with a revision of 0, which the registry's assertion
never matches, so adding a redirect URI or changing the webmail secret
failed start-up. And the principal session test now expects
legacyProtocols (C-1, added 2026-09-21), which it had missed.

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
This commit is contained in:
2026-09-22 19:33:02 -07:00
parent 4799d191a0
commit cc6f1eb298
129 changed files with 20504 additions and 168 deletions
+90
View File
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod string;
pub mod tokenizer;
pub mod word;
use std::{borrow::Cow, fmt::Display};
use self::word::Word;
use super::{Number, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Token {
CurlyOpen,
CurlyClose,
BracketOpen,
BracketClose,
ParenthesisOpen,
ParenthesisClose,
Comma,
Semicolon,
StringConstant(StringConstant),
StringVariable(Vec<u8>),
Number(usize),
Identifier(Word),
Tag(Word),
Unknown(String),
Colon,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StringConstant {
String(String),
Number(Number),
}
impl StringConstant {
pub fn to_string(&'_ self) -> Cow<'_, str> {
match self {
StringConstant::String(s) => s.as_str().into(),
StringConstant::Number(n) => n.to_string().into(),
}
}
pub fn into_string(self) -> String {
match self {
StringConstant::String(s) => s,
StringConstant::Number(n) => n.to_string(),
}
}
}
impl From<StringConstant> for Value {
fn from(value: StringConstant) -> Self {
match value {
StringConstant::String(s) => Value::Text(s.into()),
StringConstant::Number(n) => Value::Number(n),
}
}
}
impl Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Token::CurlyOpen => f.write_str("{"),
Token::CurlyClose => f.write_str("}"),
Token::BracketOpen => f.write_str("["),
Token::BracketClose => f.write_str("]"),
Token::ParenthesisOpen => f.write_str("("),
Token::ParenthesisClose => f.write_str(")"),
Token::Comma => f.write_str(","),
Token::Semicolon => f.write_str(";"),
Token::Colon => f.write_str(":"),
Token::Number(n) => write!(f, "{n}"),
Token::Identifier(w) => w.fmt(f),
Token::Tag(t) => write!(f, ":{t}"),
Token::Unknown(s) => f.write_str(s),
Token::StringVariable(s) => f.write_str(&String::from_utf8_lossy(s)),
Token::StringConstant(c) => match c {
StringConstant::String(s) => f.write_str(s),
StringConstant::Number(n) => write!(f, "{n}"),
},
}
}
}
+922
View File
@@ -0,0 +1,922 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::fmt::Display;
use mail_parser::HeaderName;
use crate::{
Envelope, MAX_MATCH_VARIABLES,
compiler::{
ContentTypePart, ErrorType, HeaderPart, HeaderVariable, MessagePart, Number,
ReceivedHostname, ReceivedPart, Value, VariableType,
grammar::{
AddressPart,
expr::{self},
instruction::CompilerState,
},
},
runtime::eval::IntoString,
};
enum State {
None,
Variable,
Encoded {
is_unicode: bool,
initial_buf_size: usize,
},
}
impl CompilerState<'_> {
pub(crate) fn tokenize_string(
&mut self,
bytes: &[u8],
parse_decoded: bool,
) -> Result<Value, ErrorType> {
let mut state = State::None;
let mut items = Vec::with_capacity(3);
let mut last_ch = 0;
let mut var_start_pos = usize::MAX;
let mut var_is_number = true;
let mut var_has_namespace = false;
let mut text_has_digits = true;
let mut text_has_dots = false;
let mut hex_start = usize::MAX;
let mut decode_buf = Vec::with_capacity(bytes.len());
for (pos, &ch) in bytes.iter().enumerate() {
let mut is_var_error = false;
match state {
State::None => match ch {
b'{' if last_ch == b'$' => {
decode_buf.pop();
var_start_pos = pos + 1;
var_is_number = true;
var_has_namespace = false;
state = State::Variable;
}
b'.' => {
if text_has_dots {
text_has_digits = false;
} else {
text_has_dots = true;
}
decode_buf.push(ch);
}
b'0'..=b'9' => {
decode_buf.push(ch);
}
_ => {
text_has_digits = false;
decode_buf.push(ch);
}
},
State::Variable => match ch {
b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'[' | b']' | b'*' | b'-' => {
var_is_number = false;
}
b'.' => {
var_is_number = false;
var_has_namespace = true;
}
b'0'..=b'9' => {}
b'}' if pos > var_start_pos => {
// Add any text before the variable
if !decode_buf.is_empty() {
self.add_value(
&mut items,
&decode_buf,
parse_decoded,
text_has_digits,
text_has_dots,
)?;
decode_buf.clear();
text_has_digits = true;
text_has_dots = false;
}
// Parse variable type
let var_name = std::str::from_utf8(&bytes[var_start_pos..pos]).unwrap();
let var_type = if !var_is_number {
self.parse_variable(var_name, var_has_namespace)
} else {
self.parse_match_variable(var_name)
};
match var_type {
Ok(Some(var)) => items.push(Value::Variable(var)),
Ok(None) => {}
Err(ErrorType::InvalidNamespace(_) | ErrorType::InvalidEnvelope(_)) => {
is_var_error = true;
}
Err(e) => return Err(e),
}
state = State::None;
}
b':' => {
if parse_decoded && !var_has_namespace {
match bytes.get(var_start_pos..pos) {
Some(enc) if enc.eq_ignore_ascii_case(b"hex") => {
state = State::Encoded {
is_unicode: false,
initial_buf_size: decode_buf.len(),
};
}
Some(enc) if enc.eq_ignore_ascii_case(b"unicode") => {
state = State::Encoded {
is_unicode: true,
initial_buf_size: decode_buf.len(),
};
}
_ => {
is_var_error = true;
}
}
} else if var_has_namespace {
var_is_number = false;
} else {
is_var_error = true;
}
}
_ => {
is_var_error = true;
}
},
State::Encoded {
is_unicode,
initial_buf_size,
} => match ch {
b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
if hex_start == usize::MAX {
hex_start = pos;
}
}
b' ' | b'\t' | b'\r' | b'\n' | b'}' => {
if hex_start != usize::MAX {
let code = std::str::from_utf8(&bytes[hex_start..pos]).unwrap();
hex_start = usize::MAX;
if !is_unicode {
if let Ok(ch) = u8::from_str_radix(code, 16) {
decode_buf.push(ch);
} else {
is_var_error = true;
}
} else if let Ok(ch) = u32::from_str_radix(code, 16) {
let mut buf = [0; 4];
decode_buf.extend_from_slice(
char::from_u32(ch)
.ok_or(ErrorType::InvalidUnicodeSequence(ch))?
.encode_utf8(&mut buf)
.as_bytes(),
);
} else {
is_var_error = true;
}
}
if ch == b'}' {
if decode_buf.len() != initial_buf_size {
state = State::None;
} else {
is_var_error = true;
}
}
}
_ => {
is_var_error = true;
}
},
}
if is_var_error {
if let State::Encoded {
initial_buf_size, ..
} = state
&& initial_buf_size != decode_buf.len()
{
decode_buf.truncate(initial_buf_size);
}
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..pos + 1]);
hex_start = usize::MAX;
state = State::None;
}
last_ch = ch;
}
match state {
State::Variable => {
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
}
State::Encoded {
initial_buf_size, ..
} => {
if initial_buf_size != decode_buf.len() {
decode_buf.truncate(initial_buf_size);
}
decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
}
State::None => (),
}
if !decode_buf.is_empty() {
self.add_value(
&mut items,
&decode_buf,
parse_decoded,
text_has_digits,
text_has_dots,
)?;
}
Ok(match items.len() {
1 => items.pop().unwrap(),
0 => Value::Text(String::new().into()),
_ => Value::List(items),
})
}
fn parse_match_variable(&mut self, var_name: &str) -> Result<Option<VariableType>, ErrorType> {
let num = var_name
.parse()
.map_err(|_| ErrorType::InvalidNumber(var_name.to_string()))?;
if num < MAX_MATCH_VARIABLES as usize {
if self.register_match_var(num) {
let total_vars = num + 1;
if total_vars > self.vars_match_max {
self.vars_match_max = total_vars;
}
Ok(Some(VariableType::Match(num)))
} else {
Ok(None)
}
} else {
Err(ErrorType::InvalidMatchVariable(num))
}
}
pub fn parse_variable(
&self,
var_name: &str,
maybe_namespace: bool,
) -> Result<Option<VariableType>, ErrorType> {
if !maybe_namespace {
if self.is_var_global(var_name) {
Ok(Some(VariableType::Global(var_name.to_string())))
} else if let Some(var_id) = self.get_local_var(var_name) {
Ok(Some(VariableType::Local(var_id)))
} else {
Ok(None)
}
} else {
let var = match var_name.to_lowercase().split_once('.') {
Some(("global" | "t", var_name)) if !var_name.is_empty() => {
VariableType::Global(var_name.to_string())
}
Some(("env", var_name)) if !var_name.is_empty() => {
VariableType::Environment(var_name.to_string())
}
Some(("envelope", var_name)) if !var_name.is_empty() => {
let envelope = match var_name {
"from" => Envelope::From,
"to" => Envelope::To,
"by_time_absolute" => Envelope::ByTimeAbsolute,
"by_time_relative" => Envelope::ByTimeRelative,
"by_mode" => Envelope::ByMode,
"by_trace" => Envelope::ByTrace,
"notify" => Envelope::Notify,
"orcpt" => Envelope::Orcpt,
"ret" => Envelope::Ret,
"envid" => Envelope::Envid,
_ => {
return Err(ErrorType::InvalidEnvelope(var_name.to_string()));
}
};
VariableType::Envelope(envelope)
}
Some(("header", var_name)) if !var_name.is_empty() => {
self.parse_header_variable(var_name)?
}
Some(("body", var_name)) if !var_name.is_empty() => match var_name {
"text" => VariableType::Part(MessagePart::TextBody(false)),
"html" => VariableType::Part(MessagePart::HtmlBody(false)),
"to_text" => VariableType::Part(MessagePart::TextBody(true)),
"to_html" => VariableType::Part(MessagePart::HtmlBody(true)),
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
},
Some(("part", var_name)) if !var_name.is_empty() => match var_name {
"text" => VariableType::Part(MessagePart::Contents),
"raw" => VariableType::Part(MessagePart::Raw),
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
},
None => {
if self.is_var_global(var_name) {
VariableType::Global(var_name.to_string())
} else if let Some(var_id) = self.get_local_var(var_name) {
VariableType::Local(var_id)
} else {
return Ok(None);
}
}
_ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
};
Ok(Some(var))
}
}
fn parse_header_variable(&self, var_name: &str) -> Result<VariableType, ErrorType> {
#[derive(Debug)]
enum State {
Name,
Index,
Part,
PartIndex,
}
let mut name = vec![];
let mut has_name = false;
let mut has_wildcard = false;
let mut hdr_name = String::new();
let mut hdr_index = String::new();
let mut part = String::new();
let mut part_index = String::new();
let mut state = State::Name;
for ch in var_name.chars() {
match state {
State::Name => match ch {
'[' => {
state = if hdr_index.is_empty() {
State::Index
} else if part.is_empty() {
State::PartIndex
} else {
return Err(ErrorType::InvalidExpression(var_name.to_string()));
};
has_name = true;
}
'.' => {
state = State::Part;
has_name = true;
}
' ' | '\t' | '\r' | '\n' => {}
'*' if !has_wildcard && hdr_name.is_empty() && name.is_empty() => {
has_wildcard = true;
}
':' if !hdr_name.is_empty() && !has_wildcard => {
name.push(
HeaderName::parse(std::mem::take(&mut hdr_name)).ok_or_else(|| {
ErrorType::InvalidExpression(var_name.to_string())
})?,
);
}
_ if !has_name && !has_wildcard => {
hdr_name.push(ch);
}
_ => {
return Err(ErrorType::InvalidExpression(var_name.to_string()));
}
},
State::Index => match ch {
']' => {
state = State::Name;
}
' ' | '\t' | '\r' | '\n' => {}
_ => {
hdr_index.push(ch);
}
},
State::Part => match ch {
'[' => {
state = State::PartIndex;
}
' ' | '\t' | '\r' | '\n' => {}
_ => {
part.push(ch);
}
},
State::PartIndex => match ch {
']' => {
state = State::Name;
}
' ' | '\t' | '\r' | '\n' => {}
_ => {
part_index.push(ch);
}
},
}
}
if !hdr_name.is_empty() {
name.push(
HeaderName::parse(hdr_name)
.ok_or_else(|| ErrorType::InvalidExpression(var_name.to_string()))?,
);
}
if !name.is_empty() || has_wildcard {
Ok(VariableType::Header(HeaderVariable {
name,
part: HeaderPart::try_from(part.as_str())
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
index_hdr: match hdr_index.as_str() {
"" => {
if !has_wildcard {
-1
} else {
0
}
}
"*" => 0,
_ => hdr_index
.parse()
.map(|v| if v == 0 { 1 } else { v })
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
},
index_part: match part_index.as_str() {
"" => {
if !has_wildcard {
-1
} else {
0
}
}
"*" => 0,
_ => part_index
.parse()
.map(|v| if v == 0 { 1 } else { v })
.map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
},
}))
} else {
Err(ErrorType::InvalidExpression(var_name.to_string()))
}
}
pub fn parse_expr_fnc_or_var(
&self,
var_name: &str,
maybe_namespace: bool,
) -> Result<expr::Token, String> {
match self.parse_variable(var_name, maybe_namespace) {
Ok(Some(var)) => Ok(expr::Token::Variable(var)),
_ => {
if let Some((id, num_args)) = self.compiler.functions.get(var_name) {
Ok(expr::Token::Function {
name: var_name.to_string(),
id: *id,
num_args: *num_args,
})
} else {
Err(format!("Invalid variable or function name {var_name:?}"))
}
}
}
}
#[inline(always)]
fn add_value(
&mut self,
items: &mut Vec<Value>,
buf: &[u8],
parse_decoded: bool,
has_digits: bool,
has_dots: bool,
) -> Result<(), ErrorType> {
if !parse_decoded {
items.push(if has_digits {
if has_dots {
match std::str::from_utf8(buf)
.ok()
.and_then(|v| (v, v.parse::<f64>().ok()?).into())
{
Some((v, n)) if n.to_string() == v => Value::Number(Number::Float(n)),
_ => Value::Text(buf.to_vec().into_string().into()),
}
} else {
match std::str::from_utf8(buf)
.ok()
.and_then(|v| (v, v.parse::<i64>().ok()?).into())
{
Some((v, n)) if n.to_string() == v => Value::Number(Number::Integer(n)),
_ => Value::Text(buf.to_vec().into_string().into()),
}
}
} else {
Value::Text(buf.to_vec().into_string().into())
});
} else {
match self.tokenize_string(buf, false)? {
Value::List(new_items) => items.extend(new_items),
item => items.push(item),
}
}
Ok(())
}
}
impl TryFrom<&str> for HeaderPart {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let (value, subvalue) = value.split_once('.').unwrap_or((value, ""));
Ok(match value {
"" | "text" => HeaderPart::Text,
// Addresses
"name" => HeaderPart::Address(AddressPart::Name),
"addr" => {
if !subvalue.is_empty() {
HeaderPart::Address(AddressPart::try_from(subvalue)?)
} else {
HeaderPart::Address(AddressPart::All)
}
}
// Content-type
"type" => HeaderPart::ContentType(ContentTypePart::Type),
"subtype" => HeaderPart::ContentType(ContentTypePart::Subtype),
"attr" if !subvalue.is_empty() => {
HeaderPart::ContentType(ContentTypePart::Attribute(subvalue.to_string()))
}
// Received
"rcvd" => {
if !subvalue.is_empty() {
HeaderPart::Received(ReceivedPart::try_from(subvalue)?)
} else {
HeaderPart::Text
}
}
// Id
"id" => HeaderPart::Id,
// Raw
"raw" => HeaderPart::Raw,
"raw_name" => HeaderPart::RawName,
// Date
"date" => HeaderPart::Date,
// Exists
"exists" => HeaderPart::Exists,
_ => {
return Err(());
}
})
}
}
impl TryFrom<&str> for ReceivedPart {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
// Received
"from" => ReceivedPart::From(ReceivedHostname::Any),
"from.name" => ReceivedPart::From(ReceivedHostname::Name),
"from.ip" => ReceivedPart::From(ReceivedHostname::Ip),
"ip" => ReceivedPart::FromIp,
"iprev" => ReceivedPart::FromIpRev,
"by" => ReceivedPart::By(ReceivedHostname::Any),
"by.name" => ReceivedPart::By(ReceivedHostname::Name),
"by.ip" => ReceivedPart::By(ReceivedHostname::Ip),
"for" => ReceivedPart::For,
"with" => ReceivedPart::With,
"tls" => ReceivedPart::TlsVersion,
"cipher" => ReceivedPart::TlsCipher,
"id" => ReceivedPart::Id,
"ident" => ReceivedPart::Ident,
"date" => ReceivedPart::Date,
"date.raw" => ReceivedPart::DateRaw,
_ => return Err(()),
})
}
}
impl TryFrom<&str> for AddressPart {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"name" => AddressPart::Name,
"addr" | "all" => AddressPart::All,
"addr.domain" => AddressPart::Domain,
"addr.local" => AddressPart::LocalPart,
"addr.user" => AddressPart::User,
"addr.detail" => AddressPart::Detail,
_ => return Err(()),
})
}
}
impl Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Text(t) => f.write_str(t),
Value::List(l) => {
for i in l {
i.fmt(f)?;
}
Ok(())
}
Value::Number(n) => n.fmt(f),
Value::Variable(v) => v.fmt(f),
Value::Regex(r) => f.write_str(&r.expr),
}
}
}
impl Display for VariableType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VariableType::Local(v) => write!(f, "${{{v}}}"),
VariableType::Match(v) => write!(f, "${{{v}}}"),
VariableType::Global(v) => write!(f, "${{global.{v}}}"),
VariableType::Environment(v) => write!(f, "${{env.{v}}}"),
VariableType::Envelope(env) => f.write_str(match env {
Envelope::From => "${{envelope.from}}",
Envelope::To => "${{envelope.to}}",
Envelope::ByTimeAbsolute => "${{envelope.by_time_absolute}}",
Envelope::ByTimeRelative => "${{envelope.by_time_relative}}",
Envelope::ByMode => "${{envelope.by_mode}}",
Envelope::ByTrace => "${{envelope.by_trace}}",
Envelope::Notify => "${{envelope.notify}}",
Envelope::Orcpt => "${{envelope.orcpt}}",
Envelope::Ret => "${{envelope.ret}}",
Envelope::Envid => "${{envelope.envit}}",
}),
VariableType::Header(hdr) => {
write!(
f,
"${{header.{}",
hdr.name.first().map(|h| h.as_str()).unwrap_or_default()
)?;
if hdr.index_hdr != 0 {
write!(f, "[{}]", hdr.index_hdr)?;
} else {
f.write_str("[*]")?;
}
/*if hdr.part != HeaderPart::Text {
f.write_str(".")?;
f.write_str(match &hdr.part {
HeaderPart::Name => "name",
HeaderPart::Address => "address",
HeaderPart::Type => "type",
HeaderPart::Subtype => "subtype",
HeaderPart::Raw => "raw",
HeaderPart::Date => "date",
HeaderPart::Attribute(attr) => attr.as_str(),
HeaderPart::Text => unreachable!(),
})?;
}*/
if hdr.index_part != 0 {
write!(f, "[{}]", hdr.index_part)?;
} else {
f.write_str("[*]")?;
}
f.write_str("}")
}
VariableType::Part(part) => {
write!(
f,
"${{{}",
match part {
MessagePart::TextBody(true) => "body.to_text",
MessagePart::TextBody(false) => "body.text",
MessagePart::HtmlBody(true) => "body.to_html",
MessagePart::HtmlBody(false) => "body.html",
MessagePart::Contents => "part.text",
MessagePart::Raw => "part.raw",
}
)?;
f.write_str("}")
}
}
}
}
#[cfg(test)]
mod tests {
use mail_parser::HeaderName;
use super::Value;
use crate::compiler::grammar::instruction::{Block, CompilerState, Instruction, MAX_PARAMS};
use crate::compiler::grammar::test::Test;
use crate::compiler::grammar::tests::test_string::TestString;
use crate::compiler::grammar::{Comparator, MatchType};
use crate::compiler::lexer::tokenizer::Tokenizer;
use crate::compiler::lexer::word::Word;
use crate::compiler::{AddressPart, HeaderPart, HeaderVariable, VariableType};
use crate::{AHashSet, Compiler};
#[test]
fn tokenize_string() {
let c = Compiler::new();
let mut block = Block::new(Word::Not);
block.match_test_pos.push(0);
let mut compiler = CompilerState {
compiler: &c,
instructions: vec![Instruction::Test(Test::String(TestString {
match_type: MatchType::Regex(u64::MAX),
comparator: Comparator::AsciiCaseMap,
source: vec![Value::Variable(VariableType::Local(0))],
key_list: vec![Value::Variable(VariableType::Local(0))],
is_not: false,
}))],
block_stack: Vec::new(),
block,
last_block_type: Word::Not,
vars_global: AHashSet::new(),
vars_num: 0,
vars_num_max: 0,
vars_local: 0,
tokens: Tokenizer::new(&c, b""),
vars_match_max: usize::MAX,
param_check: [false; MAX_PARAMS],
includes_num: 0,
};
for (input, expected_result) in [
("$${hex:24 24}", Value::Text("$$$".to_string().into())),
("$${hex:40}", Value::Text("$@".to_string().into())),
("${hex: 40 }", Value::Text("@".to_string().into())),
("${HEX: 40}", Value::Text("@".to_string().into())),
("${hex:40", Value::Text("${hex:40".to_string().into())),
("${hex:400}", Value::Text("${hex:400}".to_string().into())),
(
"${hex:4${hex:30}}",
Value::Text("${hex:40}".to_string().into()),
),
("${unicode:40}", Value::Text("@".to_string().into())),
(
"${ unicode:40}",
Value::Text("${ unicode:40}".to_string().into()),
),
("${UNICODE:40}", Value::Text("@".to_string().into())),
("${UnICoDE:0000040}", Value::Text("@".to_string().into())),
("${Unicode:40}", Value::Text("@".to_string().into())),
(
"${Unicode:40 40 ",
Value::Text("${Unicode:40 40 ".to_string().into()),
),
(
"${Unicode:Cool}",
Value::Text("${Unicode:Cool}".to_string().into()),
),
("", Value::Text("".to_string().into())),
(
"${global.full}",
Value::Variable(VariableType::Global("full".to_string())),
),
(
"${BAD${global.Company}",
Value::List(vec![
Value::Text("${BAD".to_string().into()),
Value::Variable(VariableType::Global("company".to_string())),
]),
),
(
"${President, ${global.Company} Inc.}",
Value::List(vec![
Value::Text("${President, ".to_string().into()),
Value::Variable(VariableType::Global("company".to_string())),
Value::Text(" Inc.}".to_string().into()),
]),
),
(
"dear${hex:20 24 7b}global.Name}",
Value::List(vec![
Value::Text("dear ".to_string().into()),
Value::Variable(VariableType::Global("name".to_string())),
]),
),
(
"INBOX.lists.${2}",
Value::List(vec![
Value::Text("INBOX.lists.".to_string().into()),
Value::Variable(VariableType::Match(2)),
]),
),
(
"Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test",
Value::Text("Ein unerhört großer Test".to_string().into()),
),
("&%${}!", Value::Text("&%${}!".to_string().into())),
("${doh!}", Value::Text("${doh!}".to_string().into())),
(
"${hex: 20 }${global.hi}${hex: 20 }",
Value::List(vec![
Value::Text(" ".to_string().into()),
Value::Variable(VariableType::Global("hi".to_string())),
Value::Text(" ".to_string().into()),
]),
),
(
"${hex:20 24 7b z}${global.hi}${unicode:}${unicode: }${hex:20}",
Value::List(vec![
Value::Text("${hex:20 24 7b z}".to_string().into()),
Value::Variable(VariableType::Global("hi".to_string())),
Value::Text("${unicode:}${unicode: } ".to_string().into()),
]),
),
(
"${header.from}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Text,
index_hdr: -1,
index_part: -1,
})),
),
(
"${header.from.addr}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::All),
index_hdr: -1,
index_part: -1,
})),
),
(
"${header.from[1]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Text,
index_hdr: 1,
index_part: -1,
})),
),
(
"${header.from[*]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Text,
index_hdr: 0,
index_part: -1,
})),
),
(
"${header.from[20].name}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::Name),
index_hdr: 20,
index_part: -1,
})),
),
(
"${header.from[*].addr}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::All),
index_hdr: 0,
index_part: -1,
})),
),
(
"${header.from[-5].name[2]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Address(AddressPart::Name),
index_hdr: -5,
index_part: 2,
})),
),
(
"${header.from[*].raw[*]}",
Value::Variable(VariableType::Header(HeaderVariable {
name: vec![HeaderName::From],
part: HeaderPart::Raw,
index_hdr: 0,
index_part: 0,
})),
),
] {
assert_eq!(
compiler.tokenize_string(input.as_bytes(), true).unwrap(),
expected_result,
"Failed for {input}"
);
}
for input in ["${unicode:200000}", "${Unicode:DF01}"] {
assert!(compiler.tokenize_string(input.as_bytes(), true).is_err());
}
}
}
+577
View File
@@ -0,0 +1,577 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{StringConstant, Token, word::lookup_words};
use crate::{
Compiler,
compiler::{CompileError, ErrorType, Number},
runtime::eval::IntoString,
};
use std::{iter::Peekable, slice::Iter};
pub(crate) struct Tokenizer<'x> {
pub compiler: &'x Compiler,
pub iter: Peekable<Iter<'x, u8>>,
pub buf: Vec<u8>,
pub next_token: Vec<TokenInfo>,
pub pos: usize,
pub line_num: usize,
pub line_start: usize,
pub text_line_num: usize,
pub text_line_pos: usize,
pub token_line_num: usize,
pub token_line_pos: usize,
pub token_is_tag: bool,
pub last_ch: u8,
pub state: State,
}
#[derive(Debug)]
pub(crate) struct TokenInfo {
pub(crate) token: Token,
pub(crate) line_num: usize,
pub(crate) line_pos: usize,
}
pub(crate) enum State {
None,
BracketComment,
HashComment,
QuotedString(StringType),
MultiLine(StringType),
}
#[derive(Clone, Copy, Default)]
pub(crate) struct StringType {
maybe_variable: bool,
has_other: bool,
has_digits: bool,
has_dots: bool,
}
impl<'x> Tokenizer<'x> {
pub fn new(compiler: &'x Compiler, bytes: &'x [u8]) -> Self {
Tokenizer {
compiler,
iter: bytes.iter().peekable(),
buf: Vec::with_capacity(bytes.len() / 2),
pos: usize::MAX,
line_num: 1,
line_start: 0,
text_line_num: 0,
text_line_pos: 0,
token_line_num: 0,
token_line_pos: 0,
token_is_tag: false,
next_token: Vec::with_capacity(2),
last_ch: 0,
state: State::None,
}
}
pub fn get_current_token(&mut self) -> Option<TokenInfo> {
if !self.buf.is_empty() {
let word = std::str::from_utf8(&self.buf).unwrap();
let token = if let Some(word) = lookup_words(word) {
if self.token_is_tag {
self.token_line_pos -= 1;
Token::Tag(word)
} else {
Token::Identifier(word)
}
} else if self.buf.first().unwrap().is_ascii_digit() {
let multiplier = match self.buf.last().unwrap() {
b'k' => 1024,
b'm' => 1048576,
b'g' => 1073741824,
_ => 1,
};
if let Ok(number) = (if multiplier > 1 && self.buf.len() > 1 {
std::str::from_utf8(&self.buf[..self.buf.len() - 1]).unwrap()
} else {
word
})
.parse::<usize>()
{
Token::Number(number.saturating_mul(multiplier))
} else if self.token_is_tag {
Token::Unknown(format!(":{word}"))
} else {
Token::Unknown(word.to_string())
}
} else if self.token_is_tag {
Token::Unknown(format!(":{word}"))
} else {
Token::Unknown(word.to_string())
};
self.reset_current_token();
Some(TokenInfo {
token,
line_num: self.token_line_num,
line_pos: self.token_line_pos,
})
} else {
None
}
}
#[inline(always)]
pub fn reset_current_token(&mut self) {
self.buf.clear();
self.token_is_tag = false;
}
#[inline(always)]
pub fn token_is_tag(&mut self) {
self.token_is_tag = true;
}
pub fn get_token(&mut self, token: Token) -> TokenInfo {
let next_token = TokenInfo {
token,
line_num: self.line_num,
line_pos: self.pos - self.line_start,
};
if let Some(token) = self.get_current_token() {
self.next_token.push(next_token);
token
} else {
next_token
}
}
pub fn get_string(&mut self, str_type: StringType) -> Result<TokenInfo, CompileError> {
if self.buf.len() < self.compiler.max_string_size {
let token = if str_type.maybe_variable {
Token::StringVariable(self.buf.to_vec())
} else {
let constant = self.buf.to_vec().into_string();
if !str_type.has_other && str_type.has_digits {
if !str_type.has_dots {
if let Some(number) = constant
.parse::<i64>()
.ok()
.filter(|&n| n.to_string() == constant)
{
Token::StringConstant(StringConstant::Number(Number::Integer(number)))
} else {
Token::StringConstant(StringConstant::String(constant))
}
} else if let Some(number) = constant
.parse::<f64>()
.ok()
.filter(|&n| n.to_string() == constant)
{
Token::StringConstant(StringConstant::Number(Number::Float(number)))
} else {
Token::StringConstant(StringConstant::String(constant))
}
} else {
Token::StringConstant(StringConstant::String(constant))
}
};
self.buf.clear();
Ok(TokenInfo {
token,
line_num: self.text_line_num,
line_pos: self.text_line_pos,
})
} else {
Err(CompileError {
line_num: self.text_line_num,
line_pos: self.text_line_pos,
error_type: ErrorType::StringTooLong,
})
}
}
#[inline(always)]
pub fn push_byte(&mut self, ch: u8) {
if self.buf.is_empty() {
self.token_line_num = self.line_num;
self.token_line_pos = self.pos - self.line_start;
}
self.buf.push(ch);
}
#[inline(always)]
pub fn new_line(&mut self) {
self.line_num += 1;
self.line_start = self.pos;
}
#[inline(always)]
pub fn text_start(&mut self) {
self.text_line_num = self.line_num;
self.text_line_pos = self.pos - self.line_start;
}
#[inline(always)]
pub fn is_token_start(&self) -> bool {
self.buf.is_empty()
}
#[inline(always)]
pub fn token_bytes(&self) -> &[u8] {
&self.buf
}
#[inline(always)]
pub fn next_byte(&mut self) -> Option<(u8, u8)> {
self.iter.next().map(|&ch| {
let last_ch = self.last_ch;
self.pos = self.pos.wrapping_add(1);
self.last_ch = ch;
(ch, last_ch)
})
}
#[inline(always)]
pub fn peek_byte(&mut self) -> Option<u8> {
self.iter.peek().map(|ch| **ch)
}
pub fn unwrap_next(&mut self) -> Result<TokenInfo, CompileError> {
if let Some(token) = self.next() {
token
} else {
Err(CompileError {
line_num: self.line_num,
line_pos: self.pos - self.line_start,
error_type: ErrorType::UnexpectedEOF,
})
}
}
pub fn expect_token(&mut self, token: Token) -> Result<(), CompileError> {
let next_token = self.unwrap_next()?;
if next_token.token == token {
Ok(())
} else {
Err(next_token.expected(format!("'{token}'")))
}
}
pub fn expect_static_string(&mut self) -> Result<String, CompileError> {
let next_token = self.unwrap_next()?;
match next_token.token {
Token::StringConstant(s) => Ok(s.into_string()),
Token::BracketOpen => {
let mut string = None;
loop {
let token_info = self.unwrap_next()?;
match token_info.token {
Token::StringConstant(string_) => {
string = string_.into();
}
Token::BracketClose if string.is_some() => break,
_ => return Err(token_info.expected("constant string")),
}
}
Ok(string.unwrap().into_string())
}
_ => Err(next_token.expected("constant string")),
}
}
pub fn expect_number(&mut self, max_value: usize) -> Result<usize, CompileError> {
let next_token = self.unwrap_next()?;
if let Token::Number(n) = next_token.token {
if n < max_value {
Ok(n)
} else {
Err(next_token.expected(format!("number lower than {max_value}")))
}
} else {
Err(next_token.expected("number"))
}
}
pub fn invalid_character(&self) -> CompileError {
CompileError {
line_num: self.line_num,
line_pos: self.pos - self.line_start,
error_type: ErrorType::InvalidCharacter(self.last_ch),
}
}
pub fn peek(&mut self) -> Option<Result<&TokenInfo, CompileError>> {
if self.next_token.is_empty() {
match self.next()? {
Ok(next_token) => self.next_token.push(next_token),
Err(err) => return Some(Err(err)),
}
}
self.next_token.last().map(Ok)
}
}
impl Iterator for Tokenizer<'_> {
type Item = Result<TokenInfo, CompileError>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(prev_token) = self.next_token.pop() {
return Some(Ok(prev_token));
}
'outer: while let Some((ch, last_ch)) = self.next_byte() {
match self.state {
State::None => match ch {
b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'$' => {
self.push_byte(ch);
}
b'A'..=b'Z' => {
self.push_byte(ch.to_ascii_lowercase());
}
b':' => {
if self.is_token_start()
&& matches!(self.peek_byte(), Some(b) if b.is_ascii_alphabetic())
{
self.token_is_tag();
} else if self.token_bytes().eq_ignore_ascii_case(b"text") {
self.state = State::MultiLine(StringType::default());
self.text_start();
while let Some((ch, _)) = self.next_byte() {
if ch == b'\n' {
self.new_line();
self.reset_current_token();
continue 'outer;
}
}
} else {
return Some(Ok(self.get_token(Token::Colon)));
//return Some(Err(self.invalid_character()));
}
}
b'"' => {
self.state = State::QuotedString(StringType::default());
self.text_start();
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
b'{' => {
return Some(Ok(self.get_token(Token::CurlyOpen)));
}
b'}' => {
return Some(Ok(self.get_token(Token::CurlyClose)));
}
b';' => {
return Some(Ok(self.get_token(Token::Semicolon)));
}
b',' => {
return Some(Ok(self.get_token(Token::Comma)));
}
b'[' => {
return Some(Ok(self.get_token(Token::BracketOpen)));
}
b']' => {
return Some(Ok(self.get_token(Token::BracketClose)));
}
b'(' => {
return Some(Ok(self.get_token(Token::ParenthesisOpen)));
}
b')' => {
return Some(Ok(self.get_token(Token::ParenthesisClose)));
}
b'/' => {
if let Some((b'*', _)) = self.next_byte() {
self.last_ch = 0;
self.state = State::BracketComment;
self.text_start();
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
} else {
return Some(Err(self.invalid_character()));
}
}
b'#' => {
self.state = State::HashComment;
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
b'\n' => {
self.new_line();
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
b' ' | b'\t' | b'\r' => {
if let Some(token) = self.get_current_token() {
return Some(Ok(token));
}
}
_ => {
return Some(Err(self.invalid_character()));
}
},
State::BracketComment => match ch {
b'/' if last_ch == b'*' => {
self.state = State::None;
}
b'\n' => {
self.new_line();
}
_ => (),
},
State::HashComment => {
if ch == b'\n' {
self.state = State::None;
self.new_line();
}
}
State::QuotedString(mut str_type) => match ch {
b'"' if last_ch != b'\\' => {
self.state = State::None;
return Some(self.get_string(str_type));
}
b'\n' => {
self.new_line();
self.push_byte(b'\n');
str_type.has_other = true;
self.state = State::QuotedString(str_type);
}
b'{' if (last_ch == b'$' || last_ch == b'%') => {
str_type.maybe_variable = true;
self.state = State::QuotedString(str_type);
self.push_byte(ch);
}
b'\\' => {
if last_ch == b'\\' {
self.push_byte(ch);
}
}
b'0'..=b'9' => {
if !str_type.has_digits {
str_type.has_digits = true;
self.state = State::QuotedString(str_type);
}
self.push_byte(ch);
}
b'.' => {
if !str_type.has_dots {
str_type.has_dots = true;
} else {
str_type.has_other = true;
}
self.state = State::QuotedString(str_type);
self.push_byte(ch);
}
_ => {
let ch = if last_ch == b'\\' {
match ch {
b'n' => b'\n',
b'r' => b'\r',
b't' => b'\t',
_ => ch,
}
} else {
ch
};
if !str_type.has_other && ch != b'-' {
str_type.has_other = true;
self.state = State::QuotedString(str_type);
}
self.push_byte(ch);
}
},
State::MultiLine(mut str_type) => match ch {
b'.' if last_ch == b'\n' => {
let is_eof = match (self.next_byte(), self.peek_byte()) {
(Some((b'\r', _)), Some(b'\n')) => {
self.next_byte();
true
}
(Some((b'\n', _)), _) => true,
(Some((b'.', _)), _) => {
self.push_byte(b'.');
false
}
(Some((ch, _)), _) => {
self.push_byte(b'.');
self.push_byte(ch);
false
}
_ => false,
};
if is_eof {
self.new_line();
self.state = State::None;
return Some(self.get_string(str_type));
}
}
b'\n' => {
self.new_line();
self.push_byte(b'\n');
}
b'{' if (last_ch == b'$' || last_ch == b'%') => {
str_type.maybe_variable = true;
self.state = State::MultiLine(str_type);
self.push_byte(ch);
}
b'0'..=b'9' => {
if !str_type.has_digits {
str_type.has_digits = true;
self.state = State::MultiLine(str_type);
}
self.push_byte(ch);
}
b'.' => {
if !str_type.has_dots {
str_type.has_dots = true;
} else {
str_type.has_other = true;
}
self.state = State::MultiLine(str_type);
self.push_byte(ch);
}
_ => {
if !str_type.has_other && ch != b'-' {
str_type.has_other = true;
self.state = State::MultiLine(str_type);
}
self.push_byte(ch);
}
},
}
}
match self.state {
State::BracketComment | State::QuotedString(_) | State::MultiLine(_) => {
Some(Err(CompileError {
line_num: self.text_line_num,
line_pos: self.text_line_pos,
error_type: (&self.state).into(),
}))
}
_ => None,
}
}
}
impl From<&State> for ErrorType {
fn from(state: &State) -> Self {
match state {
State::BracketComment => ErrorType::UnterminatedComment,
State::QuotedString(_) => ErrorType::UnterminatedString,
State::MultiLine(_) => ErrorType::UnterminatedMultiline,
_ => unreachable!(),
}
}
}
+410
View File
@@ -0,0 +1,410 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::fmt::Display;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum Word {
AddFlag,
AddHeader,
Address,
Addresses,
All,
AllOf,
AnyChild,
AnyOf,
Body,
Break,
ByMode,
ByTimeAbsolute,
ByTimeRelative,
ByTrace,
Comparator,
Contains,
Content,
ContentType,
Convert,
Copy,
Count,
Create,
CurrentDate,
Date,
Days,
DeleteHeader,
Detail,
Discard,
Domain,
Duplicate,
Else,
ElsIf,
Enclose,
EncodeUrl,
Envelope,
Environment,
Ereject,
Error,
Exists,
ExtractText,
False,
Fcc,
FileInto,
First,
Flags,
ForEveryPart,
From,
Global,
Handle,
HasFlag,
Header,
Headers,
If,
Ihave,
Importance,
Include,
Index,
Is,
Keep,
Last,
Length,
List,
LocalPart,
Lower,
LowerFirst,
MailboxExists,
MailboxId,
MailboxIdExists,
Matches,
Message,
Metadata,
MetadataExists,
Mime,
Name,
Not,
Notify,
NotifyMethodCapability,
Once,
Optional,
Options,
OriginalZone,
Over,
Param,
Percent,
Personal,
QuoteRegex,
QuoteWildcard,
Raw,
Redirect,
Regex,
Reject,
RemoveFlag,
Replace,
Require,
Ret,
Return,
Seconds,
ServerMetadata,
ServerMetadataExists,
Set,
SetFlag,
Size,
SpamTest,
SpecialUse,
SpecialUseExists,
Stop,
String,
Subject,
Subtype,
Text,
True,
Type,
Under,
UniqueId,
Upper,
UpperFirst,
User,
Vacation,
ValidExtList,
ValidNotifyMethod,
Value,
VirusTest,
Zone,
// Extensions
Eval,
Local,
While,
Let,
Continue,
}
pub(crate) fn lookup_words(input: &str) -> Option<Word> {
hashify::tiny_map!(
input.as_bytes(),
"addflag" => Word::AddFlag,
"addheader" => Word::AddHeader,
"address" => Word::Address,
"addresses" => Word::Addresses,
"all" => Word::All,
"allof" => Word::AllOf,
"anychild" => Word::AnyChild,
"anyof" => Word::AnyOf,
"body" => Word::Body,
"break" => Word::Break,
"bymode" => Word::ByMode,
"bytimeabsolute" => Word::ByTimeAbsolute,
"bytimerelative" => Word::ByTimeRelative,
"bytrace" => Word::ByTrace,
"comparator" => Word::Comparator,
"contains" => Word::Contains,
"content" => Word::Content,
"contenttype" => Word::ContentType,
"convert" => Word::Convert,
"copy" => Word::Copy,
"count" => Word::Count,
"create" => Word::Create,
"currentdate" => Word::CurrentDate,
"date" => Word::Date,
"days" => Word::Days,
"deleteheader" => Word::DeleteHeader,
"detail" => Word::Detail,
"discard" => Word::Discard,
"domain" => Word::Domain,
"duplicate" => Word::Duplicate,
"else" => Word::Else,
"elsif" => Word::ElsIf,
"enclose" => Word::Enclose,
"encodeurl" => Word::EncodeUrl,
"envelope" => Word::Envelope,
"environment" => Word::Environment,
"ereject" => Word::Ereject,
"error" => Word::Error,
"exists" => Word::Exists,
"extracttext" => Word::ExtractText,
"false" => Word::False,
"fcc" => Word::Fcc,
"fileinto" => Word::FileInto,
"first" => Word::First,
"flags" => Word::Flags,
"foreverypart" => Word::ForEveryPart,
"from" => Word::From,
"global" => Word::Global,
"handle" => Word::Handle,
"hasflag" => Word::HasFlag,
"header" => Word::Header,
"headers" => Word::Headers,
"if" => Word::If,
"ihave" => Word::Ihave,
"importance" => Word::Importance,
"include" => Word::Include,
"index" => Word::Index,
"is" => Word::Is,
"keep" => Word::Keep,
"last" => Word::Last,
"length" => Word::Length,
"list" => Word::List,
"localpart" => Word::LocalPart,
"lower" => Word::Lower,
"lowerfirst" => Word::LowerFirst,
"mailboxexists" => Word::MailboxExists,
"mailboxid" => Word::MailboxId,
"mailboxidexists" => Word::MailboxIdExists,
"matches" => Word::Matches,
"message" => Word::Message,
"metadata" => Word::Metadata,
"metadataexists" => Word::MetadataExists,
"mime" => Word::Mime,
"name" => Word::Name,
"not" => Word::Not,
"notify" => Word::Notify,
"notify_method_capability" => Word::NotifyMethodCapability,
"once" => Word::Once,
"optional" => Word::Optional,
"options" => Word::Options,
"originalzone" => Word::OriginalZone,
"over" => Word::Over,
"param" => Word::Param,
"percent" => Word::Percent,
"personal" => Word::Personal,
"quoteregex" => Word::QuoteRegex,
"quotewildcard" => Word::QuoteWildcard,
"raw" => Word::Raw,
"redirect" => Word::Redirect,
"regex" => Word::Regex,
"reject" => Word::Reject,
"removeflag" => Word::RemoveFlag,
"replace" => Word::Replace,
"require" => Word::Require,
"ret" => Word::Ret,
"return" => Word::Return,
"seconds" => Word::Seconds,
"servermetadata" => Word::ServerMetadata,
"servermetadataexists" => Word::ServerMetadataExists,
"set" => Word::Set,
"setflag" => Word::SetFlag,
"size" => Word::Size,
"spamtest" => Word::SpamTest,
"specialuse" => Word::SpecialUse,
"specialuse_exists" => Word::SpecialUseExists,
"stop" => Word::Stop,
"string" => Word::String,
"subject" => Word::Subject,
"subtype" => Word::Subtype,
"text" => Word::Text,
"true" => Word::True,
"type" => Word::Type,
"under" => Word::Under,
"uniqueid" => Word::UniqueId,
"upper" => Word::Upper,
"upperfirst" => Word::UpperFirst,
"user" => Word::User,
"vacation" => Word::Vacation,
"valid_ext_list" => Word::ValidExtList,
"valid_notify_method" => Word::ValidNotifyMethod,
"value" => Word::Value,
"virustest" => Word::VirusTest,
"zone" => Word::Zone,
"eval" => Word::Eval,
"local" => Word::Local,
"while" => Word::While,
"let" => Word::Let,
"continue" => Word::Continue,
)
}
impl Display for Word {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Word::AddFlag => f.write_str("addflag"),
Word::AddHeader => f.write_str("addheader"),
Word::Address => f.write_str("address"),
Word::Addresses => f.write_str("addresses"),
Word::All => f.write_str("all"),
Word::AllOf => f.write_str("allof"),
Word::AnyChild => f.write_str("anychild"),
Word::AnyOf => f.write_str("anyof"),
Word::Body => f.write_str("body"),
Word::Break => f.write_str("break"),
Word::ByMode => f.write_str("bymode"),
Word::ByTimeAbsolute => f.write_str("bytimeabsolute"),
Word::ByTimeRelative => f.write_str("bytimerelative"),
Word::ByTrace => f.write_str("bytrace"),
Word::Comparator => f.write_str("comparator"),
Word::Contains => f.write_str("contains"),
Word::Content => f.write_str("content"),
Word::ContentType => f.write_str("contenttype"),
Word::Convert => f.write_str("convert"),
Word::Copy => f.write_str("copy"),
Word::Count => f.write_str("count"),
Word::Create => f.write_str("create"),
Word::CurrentDate => f.write_str("currentdate"),
Word::Date => f.write_str("date"),
Word::Days => f.write_str("days"),
Word::DeleteHeader => f.write_str("deleteheader"),
Word::Detail => f.write_str("detail"),
Word::Discard => f.write_str("discard"),
Word::Domain => f.write_str("domain"),
Word::Duplicate => f.write_str("duplicate"),
Word::Else => f.write_str("else"),
Word::ElsIf => f.write_str("elsif"),
Word::Enclose => f.write_str("enclose"),
Word::EncodeUrl => f.write_str("encodeurl"),
Word::Envelope => f.write_str("envelope"),
Word::Environment => f.write_str("environment"),
Word::Ereject => f.write_str("ereject"),
Word::Error => f.write_str("error"),
Word::Exists => f.write_str("exists"),
Word::ExtractText => f.write_str("extracttext"),
Word::False => f.write_str("false"),
Word::Fcc => f.write_str("fcc"),
Word::FileInto => f.write_str("fileinto"),
Word::First => f.write_str("first"),
Word::Flags => f.write_str("flags"),
Word::ForEveryPart => f.write_str("foreverypart"),
Word::From => f.write_str("from"),
Word::Global => f.write_str("global"),
Word::Handle => f.write_str("handle"),
Word::HasFlag => f.write_str("hasflag"),
Word::Header => f.write_str("header"),
Word::Headers => f.write_str("headers"),
Word::If => f.write_str("if"),
Word::Ihave => f.write_str("ihave"),
Word::Importance => f.write_str("importance"),
Word::Include => f.write_str("include"),
Word::Index => f.write_str("index"),
Word::Is => f.write_str("is"),
Word::Keep => f.write_str("keep"),
Word::Last => f.write_str("last"),
Word::Length => f.write_str("length"),
Word::List => f.write_str("list"),
Word::LocalPart => f.write_str("localpart"),
Word::Lower => f.write_str("lower"),
Word::LowerFirst => f.write_str("lowerfirst"),
Word::MailboxExists => f.write_str("mailboxexists"),
Word::MailboxId => f.write_str("mailboxid"),
Word::MailboxIdExists => f.write_str("mailboxidexists"),
Word::Matches => f.write_str("matches"),
Word::Message => f.write_str("message"),
Word::Metadata => f.write_str("metadata"),
Word::MetadataExists => f.write_str("metadataexists"),
Word::Mime => f.write_str("mime"),
Word::Name => f.write_str("name"),
Word::Not => f.write_str("not"),
Word::Notify => f.write_str("notify"),
Word::NotifyMethodCapability => f.write_str("notify_method_capability"),
Word::Once => f.write_str("once"),
Word::Optional => f.write_str("optional"),
Word::Options => f.write_str("options"),
Word::OriginalZone => f.write_str("originalzone"),
Word::Over => f.write_str("over"),
Word::Param => f.write_str("param"),
Word::Percent => f.write_str("percent"),
Word::Personal => f.write_str("personal"),
Word::QuoteRegex => f.write_str("quoteregex"),
Word::QuoteWildcard => f.write_str("quotewildcard"),
Word::Raw => f.write_str("raw"),
Word::Redirect => f.write_str("redirect"),
Word::Regex => f.write_str("regex"),
Word::Reject => f.write_str("reject"),
Word::RemoveFlag => f.write_str("removeflag"),
Word::Replace => f.write_str("replace"),
Word::Require => f.write_str("require"),
Word::Ret => f.write_str("ret"),
Word::Return => f.write_str("return"),
Word::Seconds => f.write_str("seconds"),
Word::ServerMetadata => f.write_str("servermetadata"),
Word::ServerMetadataExists => f.write_str("servermetadataexists"),
Word::Set => f.write_str("set"),
Word::SetFlag => f.write_str("setflag"),
Word::Size => f.write_str("size"),
Word::SpamTest => f.write_str("spamtest"),
Word::SpecialUse => f.write_str("specialuse"),
Word::SpecialUseExists => f.write_str("specialuse_exists"),
Word::Stop => f.write_str("stop"),
Word::String => f.write_str("string"),
Word::Subject => f.write_str("subject"),
Word::Subtype => f.write_str("subtype"),
Word::Text => f.write_str("text"),
Word::True => f.write_str("true"),
Word::Type => f.write_str("type"),
Word::Under => f.write_str("under"),
Word::UniqueId => f.write_str("uniqueid"),
Word::Upper => f.write_str("upper"),
Word::UpperFirst => f.write_str("upperfirst"),
Word::User => f.write_str("user"),
Word::Vacation => f.write_str("vacation"),
Word::ValidExtList => f.write_str("valid_ext_list"),
Word::ValidNotifyMethod => f.write_str("valid_notify_method"),
Word::Value => f.write_str("value"),
Word::VirusTest => f.write_str("virustest"),
Word::Zone => f.write_str("zone"),
Word::Eval => f.write_str("eval"),
Word::Local => f.write_str("local"),
Word::While => f.write_str("while"),
Word::Let => f.write_str("let"),
Word::Continue => f.write_str("continue"),
}
}
}