use crate::core::{AsxError, ErrorCode, ErrorContext, Result, SessionContext};
#[cfg(feature = "as2")]
pub(crate) use crate::crypto::as2_smime::SmimeCipher;
use crate::send_pipeline as pipeline;
use crate::wire::enforce_payload_limit;
use std::sync::Arc;
pub(crate) mod client;
pub(crate) mod cms_keys;
#[cfg(test)]
pub(crate) use client::send_view;
pub use client::{As2, Message, PendingMdn, Received};
mod inbound;
mod ingress;
pub(crate) mod mdn;
mod mic;
mod send_client;
pub(crate) mod send_path;
mod trust;
pub(crate) mod types;
pub(crate) use trust::{As2TrustVerifier, CmsSmimeTrustVerifier};
pub(crate) use types::{
As2MdnSigningCredentials, As2MicAlgorithm, As2SendPolicy, MimeEnvelope, ParsedMdn,
};
use mdn::extract_content_type_header;
pub(crate) use mdn::mdn_report;
const MAX_AS2_MDN_BYTES: usize = 256 * 1024;
fn body_starts_with_mime_header(bytes: &[u8]) -> bool {
let line_end = bytes
.iter()
.position(|&b| b == b'\n')
.unwrap_or(bytes.len())
.min(998);
let line = &bytes[..line_end];
let line = line.strip_suffix(b"\r").unwrap_or(line);
let Some(colon) = line.iter().position(|&b| b == b':') else {
return false;
};
let name = &line[..colon];
!name.is_empty()
&& name.iter().all(|&b| {
b.is_ascii_alphanumeric()
|| matches!(b, b'!' | b'#'..=b'\'' | b'-' | b'^'..=b'`' | b'|' | b'~' | b'+' | b'.')
})
}
fn body_with_mime_headers(
_session: &SessionContext,
bytes: &Arc<[u8]>,
body_content_type: Option<&str>,
) -> Arc<[u8]> {
let Some(content_type) = body_content_type else {
return Arc::clone(bytes);
};
if body_starts_with_mime_header(bytes) {
return Arc::clone(bytes);
}
let cte: &str = if bytes.first() == Some(&0x30) {
"Content-Transfer-Encoding: binary\r\n"
} else {
""
};
let mut synthesized =
format!("MIME-Version: 1.0\r\nContent-Type: {content_type}\r\n{cte}\r\n").into_bytes();
synthesized.extend_from_slice(bytes);
synthesized.into()
}
fn decode_entity_content(
session: &SessionContext,
entity: &crate::crypto::as2_smime::VerifiedSmimeEntity,
) -> Result<Arc<[u8]>> {
let content_type = entity.content_type().unwrap_or_default();
let decoded = entity.decoded_content()?;
if crate::crypto::compression::is_as2_compressed_content_type(&content_type) {
return decompress_as2_entity(session, &decoded);
}
Ok(Arc::from(decoded.as_ref()))
}
fn decompress_as2_entity(session: &SessionContext, compressed: &[u8]) -> Result<Arc<[u8]>> {
{
let plain = crate::crypto::compression::decompress_cms_zlib(compressed).map_err(|err| {
AsxError::new(
err.code,
format!(
"failed to decompress AS2 payload (RFC 5402): {}",
err.message
),
ErrorContext::for_session("as2_receive_decompress", session),
)
})?;
Ok(Arc::from(plain))
}
}
fn normalize_message_id(id: &str) -> &str {
let id = id.trim();
let id = id.strip_prefix('<').unwrap_or(id);
id.strip_suffix('>').unwrap_or(id).trim()
}
fn split_mic(value: &str) -> Option<(&str, Vec<u8>)> {
use base64::Engine as _;
let (digest, algorithm) = value.split_once(',').unwrap_or((value, ""));
let digest = base64::engine::general_purpose::STANDARD
.decode(digest.trim().trim_matches('"'))
.ok()?;
Some((algorithm.trim().trim_matches('"'), digest))
}
#[cfg(test)]
mod tests;