diff --git a/crates/rfc5321/src/client.rs b/crates/rfc5321/src/client.rs index 25953a00..8c902c1b 100644 --- a/crates/rfc5321/src/client.rs +++ b/crates/rfc5321/src/client.rs @@ -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 { + 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()); diff --git a/crates/rfc5321/src/parser.rs b/crates/rfc5321/src/parser.rs index b2e1a01c..ca52a1b5 100644 --- a/crates/rfc5321/src/parser.rs +++ b/crates/rfc5321/src/parser.rs @@ -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 { @@ -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 { @@ -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();