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.
596 lines
21 KiB
Rust
596 lines
21 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 mail_builder::headers::{date::Date, message_id::generate_message_id_header};
|
|
use mail_parser::{decoders::quoted_printable::HEX_MAP, HeaderName};
|
|
|
|
use crate::sieve::{
|
|
compiler::grammar::actions::{
|
|
action_notify::Notify,
|
|
action_redirect::{ByTime, Ret},
|
|
},
|
|
Context, Event, Importance, Recipient,
|
|
};
|
|
|
|
use super::action_vacation::MAX_SUBJECT_LEN;
|
|
|
|
impl Notify {
|
|
pub(crate) fn exec(&self, ctx: &mut Context) {
|
|
// Do not notify on Auto-Submitted messages
|
|
for header in &ctx.message.parts[0].headers {
|
|
if matches!(&header.name, HeaderName::Other(name) if name.eq_ignore_ascii_case("Auto-Submitted"))
|
|
&& header
|
|
.value
|
|
.as_text()
|
|
.map_or(true, |v| !v.eq_ignore_ascii_case("no"))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
let uri = ctx.eval_value(&self.method).into_string();
|
|
let (scheme, params) = if let Some(parts) = parse_uri(&uri) {
|
|
parts
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
let has_fcc = self.fcc.is_some();
|
|
let is_mailto = scheme.eq_ignore_ascii_case("mailto")
|
|
&& ctx.num_out_messages < ctx.runtime.max_out_messages;
|
|
let mut events = Vec::with_capacity(3);
|
|
|
|
if is_mailto || has_fcc {
|
|
let params = if is_mailto {
|
|
if let Some(params) = parse_mailto(params) {
|
|
params
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
MailtoMessage {
|
|
to: Vec::new(),
|
|
cc: Vec::new(),
|
|
bcc: Vec::new(),
|
|
body: None,
|
|
headers: Vec::new(),
|
|
}
|
|
};
|
|
let from = if let Some(from) = &self.from {
|
|
let from = ctx.eval_value(from).into_cow();
|
|
if from
|
|
.to_ascii_lowercase()
|
|
.contains(&ctx.user_address.to_ascii_lowercase())
|
|
{
|
|
from
|
|
} else {
|
|
ctx.user_from_field().into()
|
|
}
|
|
} else {
|
|
ctx.user_from_field().into()
|
|
};
|
|
let notify_message = self.message.as_ref().map(|m| ctx.eval_value(m).into_cow());
|
|
let message_len = params
|
|
.to
|
|
.iter()
|
|
.chain(params.cc.iter())
|
|
.map(|a| a.len() + 4)
|
|
.sum::<usize>()
|
|
+ params
|
|
.headers
|
|
.iter()
|
|
.map(|(h, v)| h.len() + v.len() + 4)
|
|
.sum::<usize>()
|
|
+ params.body.as_ref().map_or(0, |b| b.len())
|
|
+ notify_message.as_ref().map_or(0, |b| b.len())
|
|
+ from.len()
|
|
+ 200;
|
|
|
|
let mut message = Vec::with_capacity(message_len);
|
|
message.extend_from_slice(b"From: ");
|
|
message.extend_from_slice(from.as_bytes());
|
|
message.extend_from_slice(b"\r\n");
|
|
|
|
for (header, addresses) in [("To: ", ¶ms.to), ("Cc: ", ¶ms.cc)] {
|
|
if !addresses.is_empty() {
|
|
message.extend_from_slice(header.as_bytes());
|
|
for (pos, address) in addresses.iter().enumerate() {
|
|
if pos > 0 {
|
|
message.extend_from_slice(b", ");
|
|
}
|
|
if !address.contains('<') {
|
|
message.push(b'<');
|
|
}
|
|
message.extend_from_slice(address.as_bytes());
|
|
if !address.contains('<') {
|
|
message.push(b'>');
|
|
}
|
|
}
|
|
message.extend_from_slice(b"\r\n");
|
|
}
|
|
}
|
|
|
|
let mut has_subject = None;
|
|
let mut has_date = false;
|
|
let mut has_message_id = false;
|
|
for (header, value) in ¶ms.headers {
|
|
match header {
|
|
HeaderName::Subject => {
|
|
has_subject = value.into();
|
|
continue;
|
|
}
|
|
HeaderName::Date => {
|
|
has_date = true;
|
|
}
|
|
HeaderName::MessageId => {
|
|
has_message_id = true;
|
|
}
|
|
HeaderName::From => {
|
|
continue;
|
|
}
|
|
_ => (),
|
|
}
|
|
message.extend_from_slice(header.as_str().as_bytes());
|
|
message.extend_from_slice(b": ");
|
|
message.extend_from_slice(value.as_bytes());
|
|
message.extend_from_slice(b"\r\n");
|
|
}
|
|
|
|
if !has_date {
|
|
message.extend_from_slice(b"Date: ");
|
|
message.extend_from_slice(Date::now().to_rfc822().as_bytes());
|
|
message.extend_from_slice(b"\r\n");
|
|
}
|
|
|
|
if !has_message_id {
|
|
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");
|
|
}
|
|
|
|
let (importance, priority) =
|
|
self.importance
|
|
.as_ref()
|
|
.map_or(("Normal", "3 (Normal)"), |i| {
|
|
match ctx.eval_value(i).into_cow().as_ref() {
|
|
"1" => ("High", "1 (High)"),
|
|
"3" => ("Low", "5 (Low)"),
|
|
_ => ("Normal", "3 (Normal)"),
|
|
}
|
|
});
|
|
message.extend_from_slice(b"Importance: ");
|
|
message.extend_from_slice(importance.as_bytes());
|
|
message.extend_from_slice(b"\r\n");
|
|
|
|
message.extend_from_slice(b"X-Priority: ");
|
|
message.extend_from_slice(priority.as_bytes());
|
|
message.extend_from_slice(b"\r\n");
|
|
|
|
message.extend_from_slice(b"Subject: ");
|
|
let subject = if let Some(subject) = has_subject {
|
|
subject.as_str()
|
|
} else if let Some(subject) = ¬ify_message {
|
|
subject.as_ref()
|
|
} else if let Some(subject) = ctx.message.subject() {
|
|
subject
|
|
} else {
|
|
""
|
|
};
|
|
let mut iter = subject.chars().enumerate();
|
|
let mut buf = [0; 4];
|
|
#[allow(clippy::while_let_on_iterator)]
|
|
while let Some((pos, char)) = iter.next() {
|
|
if pos < MAX_SUBJECT_LEN {
|
|
message.extend_from_slice(char.encode_utf8(&mut buf).as_bytes());
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if iter.next().is_some() {
|
|
message.extend_from_slice('…'.encode_utf8(&mut buf).as_bytes());
|
|
}
|
|
message.extend_from_slice(b"\r\n");
|
|
|
|
message.extend_from_slice(b"Auto-Submitted: auto-notified\r\n");
|
|
message.extend_from_slice(b"X-Sieve: yes\r\n");
|
|
message.extend_from_slice(b"Content-type: text/plain; charset=utf-8\r\n\r\n");
|
|
if let Some(body) = params.body {
|
|
message.extend_from_slice(body.as_bytes());
|
|
} else if let Some(subject) = ¬ify_message {
|
|
message.extend_from_slice(subject.as_bytes());
|
|
} else if let Some(subject) = ctx.message.subject() {
|
|
message.extend_from_slice(subject.as_bytes());
|
|
}
|
|
|
|
ctx.last_message_id += 1;
|
|
events.push(Event::CreatedMessage {
|
|
message_id: ctx.last_message_id,
|
|
message,
|
|
});
|
|
|
|
if is_mailto {
|
|
events.push(Event::SendMessage {
|
|
recipient: Recipient::Group(
|
|
params
|
|
.to
|
|
.into_iter()
|
|
.chain(params.cc)
|
|
.chain(params.bcc)
|
|
.map(|addr| {
|
|
if let Some((addr, _)) = addr
|
|
.rsplit_once('<')
|
|
.and_then(|(_, addr)| addr.rsplit_once('>'))
|
|
{
|
|
addr.to_string()
|
|
} else {
|
|
addr
|
|
}
|
|
})
|
|
.collect(),
|
|
),
|
|
notify:
|
|
crate::sieve::compiler::grammar::actions::action_redirect::Notify::Never,
|
|
return_of_content: Ret::Default,
|
|
by_time: ByTime::None,
|
|
message_id: ctx.last_message_id,
|
|
});
|
|
}
|
|
}
|
|
|
|
if !is_mailto {
|
|
events.push(Event::Notify {
|
|
method: uri,
|
|
from: self.from.as_ref().map(|f| ctx.eval_value(f).into_string()),
|
|
importance: self.importance.as_ref().map_or(Importance::Normal, |i| {
|
|
match ctx.eval_value(i).into_cow().as_ref() {
|
|
"1" => Importance::High,
|
|
"3" => Importance::Low,
|
|
_ => Importance::Normal,
|
|
}
|
|
}),
|
|
options: ctx.eval_values_owned(&self.options),
|
|
message: self
|
|
.message
|
|
.as_ref()
|
|
.map(|m| ctx.eval_value(m).into_string())
|
|
.or_else(|| ctx.message.subject().map(|s| s.to_string()))
|
|
.unwrap_or_default(),
|
|
});
|
|
ctx.num_out_messages += 1;
|
|
}
|
|
|
|
if let Some(fcc) = &self.fcc {
|
|
// File carbon copy
|
|
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();
|
|
}
|
|
}
|
|
|
|
pub fn validate_from(addr: &str) -> bool {
|
|
let mut has_at = false;
|
|
let mut has_dot = false;
|
|
let mut in_quote = false;
|
|
let mut in_angle = false;
|
|
let mut last_ch = 0;
|
|
|
|
for &ch in addr.as_bytes().iter() {
|
|
match ch {
|
|
b'\"' => {
|
|
if last_ch != b'\\' {
|
|
in_quote = !in_quote;
|
|
}
|
|
}
|
|
b'<' if !in_quote => {
|
|
if !in_angle {
|
|
in_angle = true;
|
|
has_at = false;
|
|
has_dot = false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
b'>' if !in_quote => {
|
|
if in_angle {
|
|
in_angle = false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
b'@' if !in_quote => {
|
|
if !has_at && last_ch.is_ascii_alphanumeric() {
|
|
has_at = true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
b'.' if !in_quote && has_at => {
|
|
has_dot = true;
|
|
}
|
|
_ => (),
|
|
}
|
|
last_ch = ch;
|
|
}
|
|
|
|
has_dot && has_at && !in_angle
|
|
}
|
|
|
|
pub fn validate_uri(uri: &str) -> Option<&str> {
|
|
let (scheme, uri) = parse_uri(uri)?;
|
|
if scheme.eq_ignore_ascii_case("mailto") {
|
|
parse_mailto(uri)?;
|
|
scheme.into()
|
|
} else if ["xmpp", "tel", "http", "https"].contains(&scheme) {
|
|
scheme.into()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_uri(uri: &str) -> Option<(&str, &str)> {
|
|
let (scheme, uri) = uri.split_once(':')?;
|
|
|
|
if !uri.is_empty() {
|
|
Some((scheme, uri))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub enum Mailto {
|
|
Header(HeaderName<'static>),
|
|
Body,
|
|
Other(String),
|
|
}
|
|
|
|
enum State {
|
|
Address((HeaderName<'static>, bool)),
|
|
ParamName,
|
|
ParamValue(Mailto),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MailtoMessage {
|
|
to: Vec<String>,
|
|
cc: Vec<String>,
|
|
bcc: Vec<String>,
|
|
body: Option<String>,
|
|
headers: Vec<(HeaderName<'static>, String)>,
|
|
}
|
|
|
|
fn parse_mailto(uri: &str) -> Option<MailtoMessage> {
|
|
let mut params = MailtoMessage::default();
|
|
|
|
let mut state = State::Address((HeaderName::To, false));
|
|
let mut buf = Vec::new();
|
|
let uri_ = uri.as_bytes();
|
|
let mut iter = uri_.iter();
|
|
let mut has_addresses = false;
|
|
|
|
while let Some(&ch) = iter.next() {
|
|
match ch {
|
|
b'%' => {
|
|
let hex1 = HEX_MAP[*iter.next()? as usize];
|
|
let hex2 = HEX_MAP[*iter.next()? as usize];
|
|
if hex1 != -1 && hex2 != -1 {
|
|
let ch = ((hex1 as u8) << 4) | hex2 as u8;
|
|
|
|
match &state {
|
|
State::Address((header, has_at)) => match ch {
|
|
b',' => {
|
|
if *has_at {
|
|
insert_address(
|
|
&mut params,
|
|
header.clone(),
|
|
String::from_utf8(std::mem::take(&mut buf)).ok()?,
|
|
);
|
|
has_addresses = true;
|
|
state = State::Address((header.clone(), false));
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
b'@' => {
|
|
if !*has_at {
|
|
state = State::Address((header.clone(), true));
|
|
buf.push(ch);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
_ => {
|
|
buf.push(ch);
|
|
}
|
|
},
|
|
_ => buf.push(ch),
|
|
}
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
b',' => match &state {
|
|
State::Address((header, true)) => {
|
|
insert_address(
|
|
&mut params,
|
|
header.clone(),
|
|
String::from_utf8(std::mem::take(&mut buf)).ok()?,
|
|
);
|
|
state = State::Address((header.clone(), false));
|
|
has_addresses = true;
|
|
}
|
|
State::ParamValue(_) => buf.push(ch),
|
|
_ => return None,
|
|
},
|
|
b'?' => match &state {
|
|
State::Address((header, has_at)) if *has_at || buf.is_empty() => {
|
|
if !buf.is_empty() {
|
|
insert_address(
|
|
&mut params,
|
|
header.clone(),
|
|
String::from_utf8(std::mem::take(&mut buf)).ok()?,
|
|
);
|
|
has_addresses = true;
|
|
}
|
|
state = State::ParamName;
|
|
}
|
|
State::ParamValue(_) => buf.push(ch),
|
|
_ => return None,
|
|
},
|
|
b'@' => match &state {
|
|
State::Address((header, false)) if !buf.is_empty() => {
|
|
buf.push(ch);
|
|
state = State::Address((header.clone(), true));
|
|
}
|
|
State::ParamName | State::ParamValue(_) => buf.push(ch),
|
|
_ => return None,
|
|
},
|
|
b'=' => match &state {
|
|
State::ParamName if !buf.is_empty() => {
|
|
let param = String::from_utf8(std::mem::take(&mut buf)).ok()?;
|
|
state = HeaderName::parse(param)
|
|
.map(|hdr| match hdr {
|
|
HeaderName::To | HeaderName::Cc | HeaderName::Bcc => {
|
|
State::Address((hdr, false))
|
|
}
|
|
HeaderName::Other(param) => {
|
|
if param.eq_ignore_ascii_case("body") {
|
|
State::ParamValue(Mailto::Body)
|
|
} else {
|
|
State::ParamValue(Mailto::Other(param.into_owned()))
|
|
}
|
|
}
|
|
_ => State::ParamValue(Mailto::Header(hdr)),
|
|
})
|
|
.unwrap_or_else(|| State::ParamValue(Mailto::Other(String::new())));
|
|
}
|
|
State::ParamValue(_) => buf.push(ch),
|
|
_ => return None,
|
|
},
|
|
b'&' => match state {
|
|
State::Address((header, true)) => {
|
|
if !buf.is_empty() {
|
|
insert_address(
|
|
&mut params,
|
|
header,
|
|
String::from_utf8(std::mem::take(&mut buf)).ok()?,
|
|
);
|
|
}
|
|
state = State::ParamName;
|
|
}
|
|
State::ParamValue(param) => {
|
|
if !buf.is_empty() {
|
|
let value = String::from_utf8(std::mem::take(&mut buf)).ok()?;
|
|
match param {
|
|
Mailto::Header(header) => params.headers.push((header, value)),
|
|
Mailto::Body => params.body = value.into(),
|
|
Mailto::Other(header) => params.headers.push((header.into(), value)),
|
|
}
|
|
}
|
|
state = State::ParamName;
|
|
}
|
|
_ => return None,
|
|
},
|
|
_ => match &state {
|
|
State::ParamName => {
|
|
if ch.is_ascii_alphanumeric() || [b'-', b'_'].contains(&ch) {
|
|
buf.push(ch);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
_ => {
|
|
if !ch.is_ascii_whitespace() {
|
|
buf.push(ch);
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
if !buf.is_empty() {
|
|
let value = String::from_utf8(std::mem::take(&mut buf)).ok()?;
|
|
match state {
|
|
State::Address((header, true)) => {
|
|
insert_address(&mut params, header, value);
|
|
has_addresses = true;
|
|
}
|
|
State::ParamName => {
|
|
params
|
|
.headers
|
|
.push((HeaderName::Other(value.into()), String::new()));
|
|
}
|
|
State::ParamValue(param) => match param {
|
|
Mailto::Header(header) => params.headers.push((header, value)),
|
|
Mailto::Body => params.body = value.into(),
|
|
Mailto::Other(header) => params
|
|
.headers
|
|
.push((HeaderName::Other(header.into()), value)),
|
|
},
|
|
_ => return None,
|
|
}
|
|
}
|
|
|
|
if has_addresses {
|
|
Some(params)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn insert_address(params: &mut MailtoMessage, name: HeaderName, value: String) {
|
|
if !params
|
|
.to
|
|
.iter()
|
|
.chain(params.cc.iter())
|
|
.chain(params.bcc.iter())
|
|
.any(|v| v.eq_ignore_ascii_case(&value))
|
|
{
|
|
match name {
|
|
HeaderName::To => {
|
|
params.to.push(value);
|
|
}
|
|
HeaderName::Cc => {
|
|
params.cc.push(value);
|
|
}
|
|
HeaderName::Bcc => {
|
|
params.bcc.push(value);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|