/* * 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 . * * 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, 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"); }