rfc5321 client: use SMTPUTF8/8BITMIME when we have 8bit info

This is not-quite SMTPUTF8 compliant; if we have 8bit data and
the remote host advertises the relevant capabilities, we'll
add the appropriate parameters to MAIL FROM.

It's non-compliant in that we're supposed to raise an error
if we have 8bit and the destination doesn't support it.

Here we're assuming that the remote host will raise an error
if it won't accept it.

refs: https://github.com/KumoCorp/kumomta/issues/327
This commit is contained in:
Wez Furlong
2025-01-11 06:53:44 -07:00
parent 6efff15983
commit 45802ff8a6
2 changed files with 77 additions and 14 deletions
+39 -14
View File
@@ -1,5 +1,8 @@
use crate::client_types::*;
use crate::{AsyncReadAndWrite, BoxedAsyncReadAndWrite, Command, Domain, ForwardPath, ReversePath};
use crate::{
AsyncReadAndWrite, BoxedAsyncReadAndWrite, Command, Domain, EsmtpParameter, ForwardPath,
ReversePath,
};
use hickory_proto::rr::rdata::tlsa::{CertUsage, Matching, Selector};
use hickory_proto::rr::rdata::TLSA;
use memchr::memmem::Finder;
@@ -642,15 +645,47 @@ impl SmtpClient {
recipient: RECIP,
data: B,
) -> Result<Response, ClientError> {
let sender = sender.into();
let recipient = recipient.into();
let data: &[u8] = data.as_ref();
let stuffed;
let data = match apply_dot_stuffing(data) {
Some(d) => {
stuffed = d;
&stuffed
}
None => data,
};
let data_is_8bit = data.iter().any(|&b| b >= 0x80);
let envelope_is_8bit = !sender.is_ascii() || !recipient.is_ascii();
let mut mail_from_params = vec![];
if data_is_8bit && self.capabilities.contains_key("8BITMIME") {
mail_from_params.push(EsmtpParameter {
name: "BODY".to_string(),
value: Some("8BITMIME".to_string()),
});
}
if envelope_is_8bit && self.capabilities.contains_key("SMTPUTF8") {
mail_from_params.push(EsmtpParameter {
name: "SMTPUTF8".to_string(),
value: None,
});
}
let mut responses = self
.pipeline_commands(vec![
Command::Rset,
Command::MailFrom {
address: sender.into(),
parameters: vec![],
address: sender,
parameters: mail_from_params,
},
Command::RcptTo {
address: recipient.into(),
address: recipient,
parameters: vec![],
},
Command::Data,
@@ -704,16 +739,6 @@ impl SmtpClient {
return Err(ClientError::Rejected(data_resp));
}
let data: &[u8] = data.as_ref();
let stuffed;
let data = match apply_dot_stuffing(data) {
Some(d) => {
stuffed = d;
&stuffed
}
None => data,
};
let needs_newline = data.last().map(|&b| b != b'\n').unwrap_or(true);
tracing::trace!("message data is {} bytes", data.len());
+38
View File
@@ -204,6 +204,15 @@ pub enum ReversePath {
NullSender,
}
impl ReversePath {
pub fn is_ascii(&self) -> bool {
match self {
Self::Path(path) => path.is_ascii(),
Self::NullSender => true,
}
}
}
impl TryFrom<&str> for ReversePath {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
@@ -241,6 +250,15 @@ pub enum ForwardPath {
Postmaster,
}
impl ForwardPath {
pub fn is_ascii(&self) -> bool {
match self {
Self::Path(p) => p.is_ascii(),
Self::Postmaster => true,
}
}
}
impl TryFrom<&str> for ForwardPath {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
@@ -280,6 +298,14 @@ pub struct MailPath {
pub mailbox: Mailbox,
}
impl MailPath {
pub fn is_ascii(&self) -> bool {
// Note: ignoring at_domain_list here per the to_string()
// implementation
self.mailbox.is_ascii()
}
}
impl ToString for MailPath {
fn to_string(&self) -> String {
// Note: RFC5321 says about at_domain_list:
@@ -298,6 +324,18 @@ pub struct Mailbox {
pub domain: Domain,
}
impl Mailbox {
pub fn is_ascii(&self) -> bool {
if !self.local_part.is_ascii() {
return false;
}
match &self.domain {
Domain::V4(s) | Domain::V6(s) | Domain::Name(s) => s.is_ascii(),
Domain::Tagged { tag, literal } => tag.is_ascii() && literal.is_ascii(),
}
}
}
impl ToString for Mailbox {
fn to_string(&self) -> String {
let domain = self.domain.to_string();