mirror of https://git.meli.delivery/meli/meli
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
500 lines
20 KiB
Rust
500 lines
20 KiB
Rust
/*
|
|
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
|
|
*
|
|
* This file is part of the Stalwart Sieve Interpreter.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
* in the LICENSE file at the top-level directory of this distribution.
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* You can be released from the requirements of the AGPLv3 license by
|
|
* purchasing a commercial license. Please contact licensing@stalw.art
|
|
* for more details.
|
|
*/
|
|
|
|
use std::cmp::Ordering;
|
|
|
|
use mail_parser::{
|
|
decoders::html::{html_to_text, text_to_html},
|
|
parsers::MessageStream,
|
|
Addr, Header, HeaderName, HeaderValue, Host, PartType, Received,
|
|
};
|
|
|
|
use crate::sieve::{
|
|
compiler::{
|
|
grammar::tests::test_plugin::Plugin, ContentTypePart, HeaderPart, HeaderVariable,
|
|
MessagePart, ReceivedHostname, ReceivedPart, Value, VariableType,
|
|
},
|
|
Context, Event, PluginArgument,
|
|
};
|
|
|
|
use super::Variable;
|
|
|
|
impl<'x> Context<'x> {
|
|
pub(crate) fn variable<'y: 'x>(&'y self, var: &VariableType) -> Option<Variable<'x>> {
|
|
match var {
|
|
VariableType::Local(var_num) => self.vars_local.get(*var_num).map(|v| v.as_ref()),
|
|
VariableType::Match(var_num) => self.vars_match.get(*var_num).map(|v| v.as_ref()),
|
|
VariableType::Global(var_name) => {
|
|
self.vars_global.get(var_name.as_str()).map(|v| v.as_ref())
|
|
}
|
|
VariableType::Environment(var_name) => self
|
|
.vars_env
|
|
.get(var_name.as_str())
|
|
.or_else(|| self.runtime.environment.get(var_name.as_str()))
|
|
.map(|v| v.as_ref()),
|
|
VariableType::Envelope(envelope) => self.envelope.iter().find_map(|(e, v)| {
|
|
if e == envelope {
|
|
Some(v.as_ref())
|
|
} else {
|
|
None
|
|
}
|
|
}),
|
|
VariableType::Header(header) => self.eval_header(header),
|
|
VariableType::Part(part) => match part {
|
|
MessagePart::TextBody(convert) => {
|
|
let part = self.message.parts.get(*self.message.text_body.first()?)?;
|
|
match &part.body {
|
|
PartType::Text(text) => Some(text.as_ref().into()),
|
|
PartType::Html(html) if *convert => {
|
|
Some(html_to_text(html.as_ref()).into())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
MessagePart::HtmlBody(convert) => {
|
|
let part = self.message.parts.get(*self.message.html_body.first()?)?;
|
|
match &part.body {
|
|
PartType::Html(html) => Some(html.as_ref().into()),
|
|
PartType::Text(text) if *convert => {
|
|
Some(text_to_html(text.as_ref()).into())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
MessagePart::Contents => match &self.message.parts.get(self.part)?.body {
|
|
PartType::Text(text) | PartType::Html(text) => {
|
|
Variable::from(text.as_ref()).into()
|
|
}
|
|
PartType::Binary(bin) | PartType::InlineBinary(bin) => {
|
|
Variable::from(String::from_utf8_lossy(bin.as_ref())).into()
|
|
}
|
|
_ => None,
|
|
},
|
|
MessagePart::Raw => {
|
|
let part = self.message.parts.get(self.part)?;
|
|
self.message
|
|
.raw_message()
|
|
.get(part.raw_body_offset()..part.raw_end_offset())
|
|
.map(|v| Variable::from(String::from_utf8_lossy(v)))
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(crate) fn eval_value<'z: 'y, 'y>(&'z self, string: &'y Value) -> Variable<'y> {
|
|
match string {
|
|
Value::Text(text) => Variable::String(text.into()),
|
|
Value::Variable(var) => self.variable(var).unwrap_or_default(),
|
|
Value::List(list) => {
|
|
let mut data = String::new();
|
|
for item in list {
|
|
match item {
|
|
Value::Text(string) => {
|
|
data.push_str(string);
|
|
}
|
|
Value::Variable(var) => {
|
|
if let Some(value) = self.variable(var) {
|
|
data.push_str(&value.to_cow());
|
|
}
|
|
}
|
|
Value::List(_) => {
|
|
debug_assert!(false, "This should not have happened: {string:?}");
|
|
}
|
|
Value::Number(n) => {
|
|
data.push_str(&n.to_string());
|
|
}
|
|
Value::Expression(expr) => {
|
|
if let Some(value) = self.eval_expression(expr) {
|
|
data.push_str(&value.to_string());
|
|
}
|
|
}
|
|
Value::Regex(_) => (),
|
|
}
|
|
}
|
|
data.into()
|
|
}
|
|
Value::Number(n) => Variable::from(*n),
|
|
Value::Expression(expr) => self.eval_expression(expr).unwrap_or(Variable::default()),
|
|
Value::Regex(r) => Variable::StringRef(&r.expr),
|
|
}
|
|
}
|
|
|
|
fn eval_header<'z: 'x>(&'z self, header: &HeaderVariable) -> Option<Variable<'x>> {
|
|
let mut result = Vec::new();
|
|
let part = self.message.part(self.part)?;
|
|
let raw = self.message.raw_message();
|
|
if !header.name.is_empty() {
|
|
let mut headers = part
|
|
.headers
|
|
.iter()
|
|
.filter(|h| header.name.contains(&h.name));
|
|
match header.index_hdr.cmp(&0) {
|
|
Ordering::Greater => {
|
|
if let Some(h) = headers.nth((header.index_hdr - 1) as usize) {
|
|
header.eval_part(h, raw, &mut result);
|
|
}
|
|
}
|
|
Ordering::Less => {
|
|
if let Some(h) = headers
|
|
.rev()
|
|
.nth((header.index_hdr.unsigned_abs() - 1) as usize)
|
|
{
|
|
header.eval_part(h, raw, &mut result);
|
|
}
|
|
}
|
|
Ordering::Equal => {
|
|
for h in headers {
|
|
header.eval_part(h, raw, &mut result);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for h in &part.headers {
|
|
match &header.part {
|
|
HeaderPart::Raw => {
|
|
if let Some(var) = raw
|
|
.get(h.offset_field..h.offset_end)
|
|
.map(sanitize_raw_header)
|
|
{
|
|
result.push(Variable::from(var));
|
|
}
|
|
}
|
|
HeaderPart::Text => {
|
|
if let HeaderValue::Text(text) = &h.value {
|
|
result.push(Variable::from(format!("{}: {}", h.name.as_str(), text)));
|
|
} else if let HeaderValue::Text(text) =
|
|
MessageStream::new(raw.get(h.offset_start..h.offset_end).unwrap_or(b""))
|
|
.parse_unstructured()
|
|
{
|
|
result.push(Variable::from(format!("{}: {}", h.name.as_str(), text)));
|
|
}
|
|
}
|
|
_ => {
|
|
header.eval_part(h, raw, &mut result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
match result.len() {
|
|
1 => result.pop(),
|
|
0 => None,
|
|
_ => Some(Variable::Array(result)),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn eval_values<'z: 'y, 'y>(&'z self, strings: &'y [Value]) -> Vec<Variable<'y>> {
|
|
strings.iter().map(|s| self.eval_value(s)).collect()
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn eval_values_owned(&self, strings: &[Value]) -> Vec<String> {
|
|
strings
|
|
.iter()
|
|
.map(|s| self.eval_value(s).into_cow().into_owned())
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn eval_plugin_arguments(&self, plugin: &Plugin) -> Event {
|
|
let mut arguments = Vec::with_capacity(plugin.arguments.len());
|
|
for argument in &plugin.arguments {
|
|
arguments.push(match argument {
|
|
PluginArgument::Tag(tag) => PluginArgument::Tag(*tag),
|
|
PluginArgument::Text(t) => PluginArgument::Text(self.eval_value(t).into_string()),
|
|
PluginArgument::Number(n) => PluginArgument::Number(self.eval_value(n).to_number()),
|
|
PluginArgument::Regex(r) => PluginArgument::Regex(r.clone()),
|
|
PluginArgument::Array(a) => {
|
|
let mut arr = Vec::with_capacity(a.len());
|
|
for item in a {
|
|
arr.push(match item {
|
|
PluginArgument::Tag(tag) => PluginArgument::Tag(*tag),
|
|
PluginArgument::Text(t) => {
|
|
PluginArgument::Text(self.eval_value(t).into_string())
|
|
}
|
|
PluginArgument::Number(n) => {
|
|
PluginArgument::Number(self.eval_value(n).to_number())
|
|
}
|
|
PluginArgument::Regex(r) => PluginArgument::Regex(r.clone()),
|
|
PluginArgument::Variable(var) => PluginArgument::Variable(var.clone()),
|
|
PluginArgument::Array(_) => continue,
|
|
});
|
|
}
|
|
PluginArgument::Array(arr)
|
|
}
|
|
PluginArgument::Variable(var) => PluginArgument::Variable(var.clone()),
|
|
});
|
|
}
|
|
|
|
Event::Plugin {
|
|
id: plugin.id,
|
|
arguments,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HeaderVariable {
|
|
fn eval_part<'x>(&self, header: &'x Header<'x>, raw: &'x [u8], result: &mut Vec<Variable<'x>>) {
|
|
let var = match &self.part {
|
|
HeaderPart::Text => match &header.value {
|
|
HeaderValue::Text(v) if self.include_single_part() => {
|
|
Some(Variable::from(v.as_ref()))
|
|
}
|
|
HeaderValue::TextList(list) => match self.index_part.cmp(&0) {
|
|
Ordering::Greater => list
|
|
.get((self.index_part - 1) as usize)
|
|
.map(|v| Variable::from(v.as_ref())),
|
|
Ordering::Less => list
|
|
.iter()
|
|
.rev()
|
|
.nth((self.index_part.unsigned_abs() - 1) as usize)
|
|
.map(|v| Variable::from(v.as_ref())),
|
|
Ordering::Equal => {
|
|
for item in list {
|
|
result.push(Variable::from(item.as_ref()));
|
|
}
|
|
return;
|
|
}
|
|
},
|
|
HeaderValue::ContentType(ct) => if let Some(st) = &ct.c_subtype {
|
|
Variable::from(format!("{}/{}", ct.c_type, st))
|
|
} else {
|
|
Variable::from(ct.c_type.as_ref())
|
|
}
|
|
.into(),
|
|
HeaderValue::Address(list) => {
|
|
let mut list = list.iter();
|
|
match self.index_part.cmp(&0) {
|
|
Ordering::Greater => list
|
|
.nth((self.index_part - 1) as usize)
|
|
.map(|a| a.to_text()),
|
|
Ordering::Less => list
|
|
.rev()
|
|
.nth((self.index_part.unsigned_abs() - 1) as usize)
|
|
.map(|a| a.to_text()),
|
|
Ordering::Equal => {
|
|
for item in list {
|
|
result.push(item.to_text());
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
HeaderValue::DateTime(_) => raw
|
|
.get(header.offset_start..header.offset_end)
|
|
.and_then(|bytes| std::str::from_utf8(bytes).ok())
|
|
.map(|s| s.trim())
|
|
.map(Variable::from),
|
|
_ => None,
|
|
},
|
|
HeaderPart::Address(addr) => match &header.value {
|
|
HeaderValue::Address(list) => {
|
|
let mut list = list.iter();
|
|
match self.index_part.cmp(&0) {
|
|
Ordering::Greater => list
|
|
.nth((self.index_part - 1) as usize)
|
|
.and_then(|a| addr.eval(a))
|
|
.map(Variable::from),
|
|
Ordering::Less => list
|
|
.rev()
|
|
.nth((self.index_part.unsigned_abs() - 1) as usize)
|
|
.and_then(|a| addr.eval(a))
|
|
.map(Variable::from),
|
|
Ordering::Equal => {
|
|
for item in list {
|
|
if let Some(part) = addr.eval(item) {
|
|
result.push(Variable::from(part));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
_ => None,
|
|
},
|
|
HeaderPart::Date => {
|
|
if let HeaderValue::DateTime(dt) = &header.value {
|
|
Variable::from(dt.to_timestamp()).into()
|
|
} else {
|
|
raw.get(header.offset_start..header.offset_end)
|
|
.and_then(|bytes| match MessageStream::new(bytes).parse_date() {
|
|
HeaderValue::DateTime(dt) => Variable::from(dt.to_timestamp()).into(),
|
|
_ => None,
|
|
})
|
|
}
|
|
}
|
|
HeaderPart::Id => match &header.name {
|
|
HeaderName::MessageId | HeaderName::ResentMessageId => match &header.value {
|
|
HeaderValue::Text(id) => Variable::from(id.as_ref()).into(),
|
|
HeaderValue::TextList(ids) => {
|
|
for id in ids {
|
|
result.push(Variable::from(id.as_ref()));
|
|
}
|
|
return;
|
|
}
|
|
_ => None,
|
|
},
|
|
HeaderName::Other(_) => {
|
|
match MessageStream::new(
|
|
raw.get(header.offset_start..header.offset_end)
|
|
.unwrap_or(b""),
|
|
)
|
|
.parse_id()
|
|
{
|
|
HeaderValue::Text(id) => Variable::from(id).into(),
|
|
HeaderValue::TextList(ids) => {
|
|
for id in ids {
|
|
result.push(Variable::from(id));
|
|
}
|
|
return;
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
_ => None,
|
|
},
|
|
|
|
HeaderPart::Raw => raw
|
|
.get(header.offset_start..header.offset_end)
|
|
.map(sanitize_raw_header)
|
|
.map(Variable::from),
|
|
_ => match (&header.value, &self.part) {
|
|
(HeaderValue::ContentType(ct), HeaderPart::ContentType(part)) => match part {
|
|
ContentTypePart::Type => Variable::from(ct.c_type.as_ref()).into(),
|
|
ContentTypePart::Subtype => {
|
|
ct.c_subtype.as_ref().map(|s| Variable::from(s.as_ref()))
|
|
}
|
|
ContentTypePart::Attribute(attr) => ct.attributes.as_ref().and_then(|attrs| {
|
|
attrs.iter().find_map(|(k, v)| {
|
|
if k.eq_ignore_ascii_case(attr) {
|
|
Some(Variable::from(v.as_ref()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}),
|
|
},
|
|
(HeaderValue::Received(rcvd), HeaderPart::Received(part)) => part.eval(rcvd),
|
|
_ => None,
|
|
},
|
|
};
|
|
|
|
result.push(var.unwrap_or_default());
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn include_single_part(&self) -> bool {
|
|
[-1, 0, 1].contains(&self.index_part)
|
|
}
|
|
}
|
|
|
|
impl ReceivedPart {
|
|
pub fn eval<'x>(&self, rcvd: &'x Received<'x>) -> Option<Variable<'x>> {
|
|
match self {
|
|
ReceivedPart::From(from) => rcvd
|
|
.from()
|
|
.or_else(|| rcvd.helo())
|
|
.and_then(|v| from.to_variable(v)),
|
|
ReceivedPart::FromIp => rcvd.from_ip().map(|ip| Variable::from(ip.to_string())),
|
|
ReceivedPart::FromIpRev => rcvd.from_iprev().map(Variable::from),
|
|
ReceivedPart::By(by) => rcvd.by().and_then(|v: &Host<'_>| by.to_variable(v)),
|
|
ReceivedPart::For => rcvd.for_().map(Variable::from),
|
|
ReceivedPart::With => rcvd.with().map(|v| Variable::from(v.as_str())),
|
|
ReceivedPart::TlsVersion => rcvd.tls_version().map(|v| Variable::from(v.as_str())),
|
|
ReceivedPart::TlsCipher => rcvd.tls_cipher().map(Variable::from),
|
|
ReceivedPart::Id => rcvd.id().map(Variable::from),
|
|
ReceivedPart::Ident => rcvd.ident().map(Variable::from),
|
|
ReceivedPart::Via => rcvd.via().map(Variable::from),
|
|
ReceivedPart::Date => rcvd.date().map(|d| Variable::from(d.to_timestamp())),
|
|
ReceivedPart::DateRaw => rcvd.date().map(|d| Variable::from(d.to_rfc822())),
|
|
}
|
|
}
|
|
}
|
|
|
|
trait AddrToText<'x> {
|
|
fn to_text<'z: 'x>(&'z self) -> Variable<'x>;
|
|
}
|
|
|
|
impl<'x> AddrToText<'x> for Addr<'x> {
|
|
fn to_text<'z: 'x>(&'z self) -> Variable<'x> {
|
|
if let Some(name) = &self.name {
|
|
if let Some(address) = &self.address {
|
|
Variable::String(format!("{name} <{address}>"))
|
|
} else {
|
|
Variable::StringRef(name.as_ref())
|
|
}
|
|
} else if let Some(address) = &self.address {
|
|
Variable::String(format!("<{address}>"))
|
|
} else {
|
|
Variable::StringRef("")
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ReceivedHostname {
|
|
fn to_variable<'x>(&self, host: &'x Host<'x>) -> Option<Variable<'x>> {
|
|
match (self, host) {
|
|
(ReceivedHostname::Name, Host::Name(name)) => Variable::from(name.as_ref()).into(),
|
|
(ReceivedHostname::Ip, Host::IpAddr(ip)) => Variable::from(ip.to_string()).into(),
|
|
(ReceivedHostname::Any, _) => Variable::from(host.to_string()).into(),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) trait IntoString: Sized {
|
|
fn into_string(self) -> String;
|
|
}
|
|
|
|
pub(crate) trait ToString: Sized {
|
|
fn to_string(&self) -> String;
|
|
}
|
|
|
|
impl IntoString for Vec<u8> {
|
|
fn into_string(self) -> String {
|
|
String::from_utf8(self)
|
|
.unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
|
|
}
|
|
}
|
|
|
|
fn sanitize_raw_header(bytes: &[u8]) -> String {
|
|
let mut result = Vec::with_capacity(bytes.len());
|
|
let mut last_is_space = false;
|
|
|
|
for &ch in bytes {
|
|
if ch.is_ascii_whitespace() {
|
|
last_is_space = true;
|
|
} else {
|
|
if last_is_space {
|
|
if !result.is_empty() {
|
|
result.push(b' ');
|
|
}
|
|
last_is_space = false;
|
|
}
|
|
result.push(ch);
|
|
}
|
|
}
|
|
|
|
result.into_string()
|
|
}
|