Rename the identifiers that carried the upstream name
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:
+90
@@ -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
@@ -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
@@ -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
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user