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.
361 lines
13 KiB
Rust
361 lines
13 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::borrow::Cow;
|
|
|
|
use mail_builder::headers::{date::Date, message_id::generate_message_id_header};
|
|
use mail_parser::{HeaderName, HeaderValue};
|
|
|
|
use crate::sieve::{
|
|
compiler::grammar::{
|
|
actions::{
|
|
action_redirect::{ByTime, Notify, Ret},
|
|
action_vacation::{Period, TestVacation, Vacation},
|
|
},
|
|
AddressPart,
|
|
},
|
|
runtime::tests::TestResult,
|
|
Context, Envelope, Event, Recipient,
|
|
};
|
|
|
|
pub(crate) const MAX_SUBJECT_LEN: usize = 256;
|
|
|
|
impl TestVacation {
|
|
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
|
let mut from = String::new();
|
|
let mut user_addresses = Vec::new();
|
|
|
|
if ctx.num_out_messages >= ctx.runtime.max_out_messages {
|
|
return TestResult::Bool(false);
|
|
}
|
|
|
|
for (name, value) in &ctx.envelope {
|
|
if !value.is_empty() {
|
|
match name {
|
|
Envelope::From => {
|
|
from = value.to_cow().to_ascii_lowercase();
|
|
}
|
|
Envelope::To => {
|
|
if !ctx.runtime.vacation_use_orig_rcpt {
|
|
user_addresses.push(value.to_cow());
|
|
}
|
|
}
|
|
Envelope::Orcpt => {
|
|
if ctx.runtime.vacation_use_orig_rcpt {
|
|
user_addresses.push(value.to_cow());
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add user specified addresses
|
|
for address in &self.addresses {
|
|
let address = ctx.eval_value(address).into_cow();
|
|
if !address.is_empty() {
|
|
user_addresses.push(address);
|
|
}
|
|
}
|
|
if !ctx.user_address.is_empty() {
|
|
user_addresses.push(ctx.user_address.as_ref().into());
|
|
}
|
|
|
|
// Do not reply to own address
|
|
if from.is_empty()
|
|
|| user_addresses.is_empty()
|
|
|| from.starts_with("mailer-daemon")
|
|
|| from.starts_with("owner-")
|
|
|| from.contains("-request@")
|
|
|| user_addresses.iter().any(|a| a.eq_ignore_ascii_case(&from))
|
|
{
|
|
return TestResult::Bool(false);
|
|
}
|
|
|
|
// Check headers
|
|
let mut found_rcpt = false;
|
|
let mut received_count = 0;
|
|
for header in &ctx.message.parts[0].headers {
|
|
match &header.name {
|
|
HeaderName::To
|
|
| HeaderName::Cc
|
|
| HeaderName::Bcc
|
|
| HeaderName::ResentTo
|
|
| HeaderName::ResentBcc
|
|
| HeaderName::ResentCc
|
|
if !found_rcpt =>
|
|
{
|
|
found_rcpt = ctx.find_addresses(header, &AddressPart::All, |addr| {
|
|
user_addresses.iter().any(|a| a.eq_ignore_ascii_case(addr))
|
|
});
|
|
}
|
|
HeaderName::ListArchive
|
|
| HeaderName::ListHelp
|
|
| HeaderName::ListId
|
|
| HeaderName::ListOwner
|
|
| HeaderName::ListPost
|
|
| HeaderName::ListSubscribe
|
|
| HeaderName::ListUnsubscribe => {
|
|
// Do not send vacation responses to lists
|
|
return TestResult::Bool(false);
|
|
}
|
|
HeaderName::Received => {
|
|
received_count += 1;
|
|
}
|
|
HeaderName::Other(header_name) => {
|
|
if header_name.eq_ignore_ascii_case("Auto-Submitted") {
|
|
if header
|
|
.value
|
|
.as_text()
|
|
.map_or(true, |v| !v.eq_ignore_ascii_case("no"))
|
|
{
|
|
return TestResult::Bool(false);
|
|
}
|
|
} else if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") {
|
|
if header.value.as_text().map_or(false, |v| {
|
|
v.to_ascii_lowercase()
|
|
.split(',')
|
|
.any(|v| ["all", "oof"].contains(&v.trim()))
|
|
}) {
|
|
return TestResult::Bool(false);
|
|
}
|
|
} else if header_name.eq_ignore_ascii_case("Precedence")
|
|
&& header
|
|
.value
|
|
.as_text()
|
|
.map_or(false, |v| v.eq_ignore_ascii_case("bulk"))
|
|
{
|
|
return TestResult::Bool(false);
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
// No user address found in header or possible loop
|
|
if found_rcpt && received_count <= ctx.runtime.max_received_headers {
|
|
TestResult::Event {
|
|
event: Event::DuplicateId {
|
|
id: if let Some(handle) = &self.handle {
|
|
format!("_v{}{}", from, ctx.eval_value(handle).into_cow())
|
|
} else {
|
|
format!("_v{}{}", from, ctx.eval_value(&self.reason).into_cow())
|
|
},
|
|
expiry: match &self.period {
|
|
Period::Days(days) => days * 86400,
|
|
Period::Seconds(seconds) => *seconds,
|
|
Period::Default => ctx.runtime.default_vacation_expiry,
|
|
},
|
|
last: false,
|
|
},
|
|
is_not: true,
|
|
}
|
|
} else {
|
|
TestResult::Bool(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Vacation {
|
|
pub(crate) fn exec(&self, ctx: &mut Context) {
|
|
let mut vacation_to = Cow::from("");
|
|
|
|
for (name, value) in &ctx.envelope {
|
|
if !value.is_empty() && name == &Envelope::From {
|
|
vacation_to = value.to_cow();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check headers
|
|
let mut vacation_subject = if let Some(subject) = &self.subject {
|
|
ctx.eval_value(subject)
|
|
} else {
|
|
"".into()
|
|
};
|
|
|
|
// Check headers
|
|
let mut message_id = None;
|
|
let mut vacation_to_full = None;
|
|
let mut references = None;
|
|
for header in &ctx.message.parts[0].headers {
|
|
match &header.name {
|
|
HeaderName::Subject if vacation_subject.is_empty() => {
|
|
if let Some(subject) = header.value.as_text() {
|
|
let mut vacation_subject_ = String::with_capacity(MAX_SUBJECT_LEN);
|
|
let mut iter = ctx
|
|
.runtime
|
|
.vacation_subject_prefix
|
|
.chars()
|
|
.chain(subject.chars())
|
|
.enumerate();
|
|
|
|
#[allow(clippy::while_let_on_iterator)]
|
|
while let Some((pos, char)) = iter.next() {
|
|
if pos < MAX_SUBJECT_LEN {
|
|
vacation_subject_.push(char);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if iter.next().is_some() {
|
|
vacation_subject_.push('…');
|
|
}
|
|
vacation_subject = vacation_subject_.into();
|
|
}
|
|
}
|
|
HeaderName::MessageId => {
|
|
message_id = header.value.as_text();
|
|
}
|
|
HeaderName::References => {
|
|
if header.offset_start > 0 {
|
|
references = (&ctx.message.raw_message
|
|
[header.offset_start..header.offset_end])
|
|
.into();
|
|
}
|
|
}
|
|
HeaderName::From | HeaderName::Sender => {
|
|
if matches!(&header.value, HeaderValue::Address(address) if address.contains(vacation_to.as_ref()))
|
|
&& header.offset_start > 0
|
|
{
|
|
vacation_to_full = (&ctx.message.raw_message
|
|
[header.offset_start..header.offset_end])
|
|
.into();
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
// Build message
|
|
let vacation_from = if let Some(from) = &self.from {
|
|
ctx.eval_value(from)
|
|
} else if !ctx.user_address.is_empty() {
|
|
ctx.user_from_field().into()
|
|
} else if let Some(addr) =
|
|
ctx.envelope
|
|
.iter()
|
|
.find_map(|(n, v)| if n == &Envelope::To { Some(v) } else { None })
|
|
{
|
|
addr.to_cow().into()
|
|
} else {
|
|
"".into()
|
|
};
|
|
if vacation_subject.is_empty() {
|
|
vacation_subject = ctx.runtime.vacation_default_subject.as_ref().into();
|
|
}
|
|
let vacation_body = ctx.eval_value(&self.reason);
|
|
let message_len = vacation_body.len()
|
|
+ vacation_from.len()
|
|
+ vacation_to_full
|
|
.as_ref()
|
|
.map_or(vacation_to.len(), |t| t.len())
|
|
+ vacation_subject.len()
|
|
+ message_id.as_ref().map_or(0, |m| m.len() * 2)
|
|
+ references.as_ref().map_or(0, |m| m.len())
|
|
+ 160;
|
|
|
|
let mut message = Vec::with_capacity(message_len);
|
|
write_header(&mut message, "From: ", vacation_from.into_cow().as_ref());
|
|
if let Some(vacation_to_full) = vacation_to_full {
|
|
message.extend_from_slice(b"To:");
|
|
message.extend_from_slice(vacation_to_full);
|
|
} else {
|
|
write_header(&mut message, "To: ", vacation_to.to_string().as_ref());
|
|
}
|
|
write_header(
|
|
&mut message,
|
|
"Subject: ",
|
|
vacation_subject.into_cow().as_ref(),
|
|
);
|
|
if let Some(message_id) = message_id {
|
|
message.extend_from_slice(b"In-Reply-To: <");
|
|
message.extend_from_slice(message_id.as_bytes());
|
|
message.extend_from_slice(b">\r\n");
|
|
|
|
message.extend_from_slice(b"References: <");
|
|
message.extend_from_slice(message_id.as_bytes());
|
|
if let Some(references) = references {
|
|
message.extend_from_slice(b"> ");
|
|
message.extend_from_slice(references);
|
|
} else {
|
|
message.extend_from_slice(b">\r\n");
|
|
}
|
|
}
|
|
message.extend_from_slice(b"Date: ");
|
|
message.extend_from_slice(Date::now().to_rfc822().as_bytes());
|
|
message.extend_from_slice(b"\r\n");
|
|
|
|
message.extend_from_slice(b"Message-ID: ");
|
|
generate_message_id_header(&mut message, &ctx.runtime.local_hostname).unwrap();
|
|
message.extend_from_slice(b"\r\n");
|
|
|
|
write_header(&mut message, "Auto-Submitted: ", "auto-replied");
|
|
if !self.mime {
|
|
message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n");
|
|
}
|
|
message.extend_from_slice(vacation_body.into_cow().as_bytes());
|
|
|
|
// Add action
|
|
let mut events = Vec::with_capacity(3);
|
|
ctx.last_message_id += 1;
|
|
ctx.num_out_messages += 1;
|
|
events.push(Event::CreatedMessage {
|
|
message_id: ctx.last_message_id,
|
|
message,
|
|
});
|
|
events.push(Event::SendMessage {
|
|
recipient: Recipient::Address(vacation_to.to_string()),
|
|
notify: Notify::Never,
|
|
return_of_content: Ret::Default,
|
|
by_time: ByTime::None,
|
|
message_id: ctx.last_message_id,
|
|
});
|
|
|
|
// File carbon copy
|
|
if let Some(fcc) = &self.fcc {
|
|
events.push(Event::FileInto {
|
|
folder: ctx.eval_value(&fcc.mailbox).into_string(),
|
|
flags: ctx.get_local_flags(&fcc.flags),
|
|
mailbox_id: fcc
|
|
.mailbox_id
|
|
.as_ref()
|
|
.map(|m| ctx.eval_value(m).into_string()),
|
|
special_use: fcc
|
|
.special_use
|
|
.as_ref()
|
|
.map(|s| ctx.eval_value(s).into_string()),
|
|
create: fcc.create,
|
|
message_id: ctx.last_message_id,
|
|
});
|
|
}
|
|
ctx.queued_events = events.into_iter();
|
|
}
|
|
}
|
|
|
|
fn write_header(buf: &mut Vec<u8>, name: &str, value: &str) {
|
|
buf.extend_from_slice(name.as_bytes());
|
|
buf.extend_from_slice(value.as_bytes());
|
|
buf.extend_from_slice(b"\r\n");
|
|
}
|