Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::queue::{MessageWrapper, Metadata, spool::QueueParams};
|
||||
use common::{
|
||||
Server,
|
||||
config::smtp::auth::{Dkim1Signer, DkimSigners},
|
||||
expr::{functions::ResolveVariable, if_block::IfBlock},
|
||||
};
|
||||
use mail_auth::{
|
||||
AuthenticatedMessage,
|
||||
common::headers::HeaderWriter,
|
||||
dkim2::{Hop, MessageInstance},
|
||||
};
|
||||
use mail_parser::{Address, parsers::MessageStream};
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
use utils::sanitize_email;
|
||||
|
||||
pub(crate) trait DkimSign: Sync + Send {
|
||||
fn sign_message(
|
||||
&self,
|
||||
message: &mut MessageWrapper,
|
||||
params: &mut QueueParams<'_, '_>,
|
||||
) -> impl Future<Output = Option<Vec<u8>>> + Send;
|
||||
|
||||
fn eval_signers(
|
||||
&self,
|
||||
if_block: &IfBlock,
|
||||
resolver: &impl ResolveVariable,
|
||||
session_id: u64,
|
||||
) -> impl Future<Output = Option<Arc<DkimSigners>>> + Send;
|
||||
}
|
||||
|
||||
impl DkimSign for Server {
|
||||
async fn sign_message(
|
||||
&self,
|
||||
message: &mut MessageWrapper,
|
||||
params: &mut QueueParams<'_, '_>,
|
||||
) -> Option<Vec<u8>> {
|
||||
let signers = params.dkim_signers.as_ref().unwrap();
|
||||
let raw_message = params.raw_message;
|
||||
|
||||
// DKIM1 signing
|
||||
let mut headers = Vec::with_capacity(64);
|
||||
for signer in &signers.dkim1 {
|
||||
let result = match (signer, params.raw_headers) {
|
||||
(Dkim1Signer::RsaSha256(signer), None) => signer.sign(raw_message),
|
||||
(Dkim1Signer::Ed25519Sha256(signer), None) => signer.sign(raw_message),
|
||||
(Dkim1Signer::RsaSha256(signer), Some(headers)) => {
|
||||
signer.sign_chained([headers, raw_message].iter().copied())
|
||||
}
|
||||
(Dkim1Signer::Ed25519Sha256(signer), Some(headers)) => {
|
||||
signer.sign_chained([headers, raw_message].iter().copied())
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(signature) => {
|
||||
signature.write_header(&mut headers);
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
trc::Error::from(err)
|
||||
.span_id(params.session_id)
|
||||
.details("Failed to sign message")
|
||||
.caused_by(trc::location!())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DKIM2 signing
|
||||
if let Some(signer) = &signers.dkim2
|
||||
&& let Some(modified) =
|
||||
AuthenticatedMessage::parse_with_opts(raw_message, params.raw_headers, true)
|
||||
{
|
||||
// Generate message instance
|
||||
let original = params.original_authenticated_message.take().or_else(|| {
|
||||
params
|
||||
.original_raw_message
|
||||
.and_then(AuthenticatedMessage::parse)
|
||||
});
|
||||
let instance = MessageInstance::from_message(&modified, original.as_ref());
|
||||
if let Some(instance) = &instance {
|
||||
instance.write_header(&mut headers);
|
||||
}
|
||||
|
||||
// Obtain disclosed and undisclosed recipients
|
||||
let envelopes = message.undisclosed_recipients(&modified);
|
||||
|
||||
// Generate DKIM2 signature for disclosed recipients
|
||||
if !envelopes.disclosed_recipients.is_empty() {
|
||||
match signer.sign_with_message_instance(
|
||||
&modified,
|
||||
instance.as_ref(),
|
||||
Hop::real(
|
||||
message.message.return_path.as_ref(),
|
||||
envelopes.disclosed_recipients,
|
||||
),
|
||||
) {
|
||||
Ok(signature) => {
|
||||
if envelopes.undisclosed_recipients.is_empty() {
|
||||
// Happy path: no undisclosed recipients, serialize signature straight to blob
|
||||
signature.write_header(&mut headers);
|
||||
} else {
|
||||
// Undisclosed recipients present, serialize signature to metadata
|
||||
let mut header = Vec::with_capacity(64);
|
||||
signature.write_header(&mut header);
|
||||
params.metadata.push(Metadata::Headers {
|
||||
value: header.into_boxed_slice(),
|
||||
id: u64::MAX,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
trc::Error::from(err)
|
||||
.span_id(params.session_id)
|
||||
.details("Failed to DKIM2 sign message")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate DKIM2 signature for undisclosed recipients
|
||||
for (pos, rcpt) in envelopes.undisclosed_recipients {
|
||||
match signer.sign_with_message_instance(
|
||||
&modified,
|
||||
instance.as_ref(),
|
||||
Hop::real(message.message.return_path.as_ref(), [rcpt]),
|
||||
) {
|
||||
Ok(signature) => {
|
||||
// Serialize signature to metadata
|
||||
let mut header = Vec::with_capacity(64);
|
||||
signature.write_header(&mut header);
|
||||
params.metadata.push(Metadata::Headers {
|
||||
value: header.into_boxed_slice(),
|
||||
id: pos as u64,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
trc::Error::from(err)
|
||||
.span_id(params.session_id)
|
||||
.details("Failed to DKIM2 sign message")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(!headers.is_empty()).then_some(headers)
|
||||
}
|
||||
|
||||
async fn eval_signers(
|
||||
&self,
|
||||
if_block: &IfBlock,
|
||||
resolver: &impl ResolveVariable,
|
||||
session_id: u64,
|
||||
) -> Option<Arc<DkimSigners>> {
|
||||
let sign_with_domain = self
|
||||
.eval_if::<String, _>(if_block, resolver, session_id)
|
||||
.await?;
|
||||
match self.dkim_signers(&sign_with_domain).await {
|
||||
Ok(signers) => signers,
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.span_id(session_id)
|
||||
.details("Failed to retrieve DKIM signers")
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Dkim2Envelopes<'x> {
|
||||
undisclosed_recipients: Vec<(usize, &'x str)>,
|
||||
disclosed_recipients: Vec<&'x str>,
|
||||
}
|
||||
|
||||
impl MessageWrapper {
|
||||
fn undisclosed_recipients<'x>(
|
||||
&'x self,
|
||||
message: &AuthenticatedMessage<'_>,
|
||||
) -> Dkim2Envelopes<'x> {
|
||||
if self.message.recipients.len() == 1 {
|
||||
return Dkim2Envelopes {
|
||||
undisclosed_recipients: Vec::new(),
|
||||
disclosed_recipients: vec![self.message.recipients[0].address.as_ref()],
|
||||
};
|
||||
}
|
||||
|
||||
let mut recipients = HashSet::with_capacity(self.message.recipients.len());
|
||||
|
||||
for addr in message.headers.iter().filter_map(|(name, value)| {
|
||||
let name = name.trim_ascii();
|
||||
if name.len() == 2
|
||||
&& (name.eq_ignore_ascii_case(b"to") || name.eq_ignore_ascii_case(b"cc"))
|
||||
{
|
||||
MessageStream::new(value).parse_address().into_address()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
match addr {
|
||||
Address::List(addrs) => {
|
||||
recipients.extend(
|
||||
addrs
|
||||
.iter()
|
||||
.filter_map(|a| a.address())
|
||||
.map(sanitize_or_lower),
|
||||
);
|
||||
}
|
||||
Address::Group(groups) => {
|
||||
for group in groups {
|
||||
recipients.extend(
|
||||
group
|
||||
.addresses
|
||||
.iter()
|
||||
.filter_map(|a| a.address())
|
||||
.map(sanitize_or_lower),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut undisclosed_recipients = Vec::new();
|
||||
let mut disclosed_recipients = Vec::new();
|
||||
for (i, rcpt) in self.message.recipients.iter().enumerate() {
|
||||
if !recipients.contains(rcpt.address.as_ref())
|
||||
&& !recipients.contains(&sanitize_or_lower(&rcpt.address))
|
||||
{
|
||||
undisclosed_recipients.push((i, rcpt.address.as_ref()));
|
||||
} else {
|
||||
disclosed_recipients.push(rcpt.address.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
Dkim2Envelopes {
|
||||
undisclosed_recipients,
|
||||
disclosed_recipients,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_or_lower(rcpt: &str) -> String {
|
||||
sanitize_email(rcpt).unwrap_or_else(|| rcpt.to_lowercase())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::queue::{Message, QueueId, Recipient};
|
||||
use common::config::smtp::queue::QueueName;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
fn wrapper(recipients: &[&str]) -> MessageWrapper {
|
||||
MessageWrapper {
|
||||
queue_id: 0 as QueueId,
|
||||
queue_name: QueueName::default(),
|
||||
is_multi_queue: false,
|
||||
span_id: 0,
|
||||
message: Message {
|
||||
created: 0,
|
||||
blob_hash: Default::default(),
|
||||
return_path: "[email protected]".into(),
|
||||
recipients: recipients.iter().map(Recipient::new).collect(),
|
||||
received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
received_via_port: 0,
|
||||
flags: 0,
|
||||
env_id: None,
|
||||
priority: 0,
|
||||
size: 0,
|
||||
metadata: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn split(headers: &str, recipients: &[&str]) -> (Vec<String>, Vec<String>) {
|
||||
let raw = format!("{headers}\r\nSubject: test\r\n\r\nbody\r\n");
|
||||
let auth = AuthenticatedMessage::parse(raw.as_bytes()).expect("parse message");
|
||||
let message = wrapper(recipients);
|
||||
let envelopes = message.undisclosed_recipients(&auth);
|
||||
let mut disclosed = envelopes
|
||||
.disclosed_recipients
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let mut undisclosed = envelopes
|
||||
.undisclosed_recipients
|
||||
.iter()
|
||||
.map(|(_, s)| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
disclosed.sort();
|
||||
undisclosed.sort();
|
||||
(disclosed, undisclosed)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_recipient_is_always_disclosed() {
|
||||
let (disclosed, undisclosed) = split("To: [email protected]", &["[email protected]"]);
|
||||
assert_eq!(disclosed, vec!["[email protected]".to_string()]);
|
||||
assert!(undisclosed.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_recipients_disclosed() {
|
||||
let (disclosed, undisclosed) = split(
|
||||
"To: [email protected], [email protected]\r\nCc: [email protected]",
|
||||
&["[email protected]", "[email protected]", "[email protected]"],
|
||||
);
|
||||
assert_eq!(
|
||||
disclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string(),
|
||||
]
|
||||
);
|
||||
assert!(undisclosed.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_disclosed_and_undisclosed() {
|
||||
let (disclosed, undisclosed) = split(
|
||||
"To: [email protected]\r\nCc: [email protected]",
|
||||
&[
|
||||
"[email protected]",
|
||||
"[email protected]",
|
||||
"[email protected]",
|
||||
"[email protected]",
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
disclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
undisclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_to_or_cc_header_all_undisclosed() {
|
||||
let (disclosed, undisclosed) = split(
|
||||
"From: [email protected]",
|
||||
&["[email protected]", "[email protected]"],
|
||||
);
|
||||
assert!(disclosed.is_empty());
|
||||
assert_eq!(
|
||||
undisclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_addresses_are_disclosed() {
|
||||
let (disclosed, undisclosed) = split(
|
||||
"To: Team:[email protected],[email protected];",
|
||||
&["[email protected]", "[email protected]", "[email protected]"],
|
||||
);
|
||||
assert_eq!(
|
||||
disclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(undisclosed, vec!["[email protected]".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_address_casing_is_ignored() {
|
||||
let (disclosed, undisclosed) = split(
|
||||
"To: [email protected], [email protected]",
|
||||
&["[email protected]", "[email protected]", "[email protected]"],
|
||||
);
|
||||
assert_eq!(
|
||||
disclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(undisclosed, vec!["[email protected]".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_names_and_brackets_are_ignored() {
|
||||
let (disclosed, undisclosed) = split(
|
||||
"To: \"Alice Doe\" <[email protected]>\r\nCc: Bob <[email protected]>",
|
||||
&["[email protected]", "[email protected]", "[email protected]"],
|
||||
);
|
||||
assert_eq!(
|
||||
disclosed,
|
||||
vec![
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(undisclosed, vec!["[email protected]".to_string()]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user