mod address;
mod headers;
mod ids;
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
pub(crate) use address::encode_rfc2047_if_needed;
pub(crate) use address::validate_address;
use std::sync::atomic::AtomicU64;
#[cfg(test)]
use std::sync::atomic::Ordering;
use base64::Engine as _;
use crate::error::Error;
use crate::parser::{parse_address_list, parse_rfc5322_date, strip_comments};
use crate::types::{
is_strict_bare_message_id_body, Address, BuiltMessage, DateTime, HeaderName,
OutgoingAttachment, OutgoingEmail,
};
use address::{
escape_quoted_string, extract_domain, format_address, format_address_list,
is_resent_extra_header, is_structured_extra_header, is_trace_extra_header, is_valid_msg_id,
normalize_line_endings, partition_resent_blocks, resent_field_kind, sanitize_header_value,
strip_angle_brackets, validate_resent_header_value, validate_reserved_header_name,
validate_trace_header_value, validate_trace_headers,
};
use headers::{try_write_header, write_attachment_part, write_boundary, write_text_part};
use ids::{generate_boundary_not_in, generate_message_id};
#[cfg(test)]
use headers::{
encode_quoted_printable, is_trailing_whitespace, is_valid_mime_type, split_header_words,
};
#[cfg(test)]
use ids::{contains_boundary, generate_boundary};
static MSG_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Clone)]
struct PendingResentHeader {
name: String,
value: String,
kind: ResentFieldKind,
}
#[derive(Clone, Copy)]
enum ResentFieldKind {
Date,
From { mailbox_count: usize },
Sender,
To,
Cc,
Bcc,
ReplyTo,
MessageId,
}
impl ResentFieldKind {
const fn slot_index(self) -> usize {
match self {
Self::Date => 0,
Self::From { .. } => 1,
Self::Sender => 2,
Self::To => 3,
Self::Cc => 4,
Self::Bcc => 5,
Self::ReplyTo => 6,
Self::MessageId => 7,
}
}
}
#[derive(Clone, Copy)]
enum ResentBlockError {
MissingRequiredFields,
MissingSender,
}
const MAX_LINE_LEN: usize = 78;
const RFC2047_LINE_LIMIT: usize = 76;
const HARD_LINE_LIMIT: usize = 998;
fn snap_utf8_chunk_end(bytes: &[u8], pos: usize, max_bytes: usize) -> usize {
let mut end = (pos + max_bytes).min(bytes.len());
while end > pos && end < bytes.len() && is_utf8_continuation(bytes[end]) {
end -= 1;
}
if end == pos && pos < bytes.len() {
end = (pos + utf8_char_len(bytes[pos])).min(bytes.len());
}
end
}
pub(crate) fn is_utf8_continuation(b: u8) -> bool {
(b & 0xC0) == 0x80
}
pub(crate) fn utf8_char_len(lead: u8) -> usize {
if lead < 0x80 {
1
} else if lead < 0xE0 {
2
} else if lead < 0xF0 {
3
} else {
4
}
}
#[allow(clippy::too_many_lines)]
pub fn build_message(email: &OutgoingEmail) -> Result<BuiltMessage, Error> {
if email.from.is_empty() {
return Err(Error::MissingFrom);
}
for addr in &email.from {
validate_address(addr)?;
}
if let Some(ref sender) = email.sender {
validate_address(sender)?;
}
for addr in &email.to {
validate_address(addr)?;
}
for addr in &email.cc {
validate_address(addr)?;
}
for addr in &email.bcc {
validate_address(addr)?;
}
for addr in &email.reply_to {
validate_address(addr)?;
}
if email.from.len() > 1 && email.sender.is_none() {
return Err(Error::MissingSender);
}
let domain_addr = email.sender.as_ref().unwrap_or(&email.from[0]);
let domain = extract_domain(&domain_addr.email).unwrap_or("daaki.local");
let message_id = generate_message_id(domain);
let mut return_path_headers: Vec<(String, String)> = Vec::new();
let mut other_trace_headers: Vec<(String, String)> = Vec::new();
let mut pending_resent_headers: Vec<PendingResentHeader> = Vec::new();
let mut regular_extra_headers: Vec<(String, String)> = Vec::new();
for (name, value) in &email.extra_headers {
validate_reserved_header_name(name)?;
let name_str = name.as_str();
let sanitized = sanitize_header_value(value);
validate_trace_header_value(name_str, &sanitized)?;
let resent_from_count = validate_resent_header_value(name_str, &sanitized)?;
let wire_value = if is_structured_extra_header(name_str) {
sanitized
} else {
encode_rfc2047_if_needed(&sanitized)
};
if is_trace_extra_header(name_str) {
if name_str.eq_ignore_ascii_case("return-path") {
return_path_headers.push((name_str.to_owned(), wire_value));
} else {
other_trace_headers.push((name_str.to_owned(), wire_value));
}
} else if is_resent_extra_header(name_str) {
pending_resent_headers.push(PendingResentHeader {
name: name_str.to_owned(),
value: wire_value,
kind: resent_field_kind(name_str, resent_from_count.unwrap_or(0))?,
});
} else {
regular_extra_headers.push((name_str.to_owned(), wire_value));
}
}
validate_trace_headers(&return_path_headers, &other_trace_headers)?;
let resent_blocks = partition_resent_blocks(&pending_resent_headers)?;
let mut raw = Vec::new();
for (name, value) in &return_path_headers {
try_write_header(&mut raw, name, value)?;
}
for (name, value) in &other_trace_headers {
try_write_header(&mut raw, name, value)?;
}
for resent_block in &resent_blocks {
for (name, value) in resent_block {
try_write_header(&mut raw, name, value)?;
}
}
try_write_header(
&mut raw,
"From",
&sanitize_header_value(&format_address_list(&email.from)),
)?;
if let Some(ref sender) = email.sender {
let emit_sender = if email.from.len() == 1 {
sender != &email.from[0]
} else {
true
};
if emit_sender {
try_write_header(
&mut raw,
"Sender",
&sanitize_header_value(&format_address(sender)),
)?;
}
}
if !email.to.is_empty() {
try_write_header(
&mut raw,
"To",
&sanitize_header_value(&format_address_list(&email.to)),
)?;
}
if !email.cc.is_empty() {
try_write_header(
&mut raw,
"Cc",
&sanitize_header_value(&format_address_list(&email.cc)),
)?;
}
if !email.reply_to.is_empty() {
try_write_header(
&mut raw,
"Reply-To",
&sanitize_header_value(&format_address_list(&email.reply_to)),
)?;
}
try_write_header(
&mut raw,
"Subject",
&encode_rfc2047_if_needed(&sanitize_header_value(&email.subject)),
)?;
let date = email.date.clone().unwrap_or_else(DateTime::now);
try_write_header(&mut raw, "Date", &date.to_rfc5322_string())?;
try_write_header(&mut raw, "Message-ID", &format!("<{message_id}>"))?;
try_write_header(&mut raw, "MIME-Version", "1.0")?;
if !email.in_reply_to.is_empty() {
let ids: Vec<String> = email
.in_reply_to
.iter()
.filter_map(|id| {
let sanitized = sanitize_header_value(id.as_str());
let bare = strip_angle_brackets(&sanitized);
if is_valid_msg_id(bare) {
Some(format!("<{bare}>"))
} else {
None
}
})
.collect();
if !ids.is_empty() {
try_write_header(&mut raw, "In-Reply-To", &ids.join(" "))?;
}
}
if !email.references.is_empty() {
let refs: Vec<String> = email
.references
.iter()
.filter_map(|id| {
let sanitized = sanitize_header_value(id.as_str());
let bare = strip_angle_brackets(&sanitized);
if is_valid_msg_id(bare) {
Some(format!("<{bare}>"))
} else {
None
}
})
.collect();
if !refs.is_empty() {
try_write_header(&mut raw, "References", &refs.join(" "))?;
}
}
for (name, value) in ®ular_extra_headers {
try_write_header(&mut raw, name, value)?;
}
let has_text = email.body_text.is_some();
let has_html = email.body_html.is_some();
let (inline_atts, regular_atts): (Vec<_>, Vec<_>) = email
.attachments
.iter()
.partition(|a| a.is_inline && a.content_id.is_some());
let has_attachments = !email.attachments.is_empty();
let encapsulated_content = {
let mut buf = Vec::new();
if let Some(ref text) = email.body_text {
buf.extend_from_slice(text.as_bytes());
}
if let Some(ref html) = email.body_html {
buf.extend_from_slice(html.as_bytes());
}
for att in &email.attachments {
buf.extend_from_slice(&att.data);
buf.extend_from_slice(att.filename.as_bytes());
}
buf
};
let mut used_boundaries: Vec<String> = Vec::new();
let new_boundary = |content: &[u8], used: &mut Vec<String>| -> String {
loop {
let b = generate_boundary_not_in(content);
if !used.contains(&b) {
used.push(b.clone());
return b;
}
}
};
if has_attachments {
let (inline_atts, regular_atts) = if has_html {
(inline_atts, regular_atts)
} else {
(Vec::new(), email.attachments.iter().collect::<Vec<_>>())
};
let has_inline = !inline_atts.is_empty();
let has_regular = !regular_atts.is_empty();
let needs_mixed = has_regular || !has_inline;
let mixed_boundary = if needs_mixed {
let b = new_boundary(&encapsulated_content, &mut used_boundaries);
try_write_header(
&mut raw,
"Content-Type",
&format!("multipart/mixed; boundary=\"{b}\""),
)?;
raw.extend_from_slice(b"\r\n");
write_boundary(&mut raw, &b, false);
Some(b)
} else {
None
};
if has_inline {
let related_boundary = new_boundary(&encapsulated_content, &mut used_boundaries);
let root_type = if has_text && has_html {
"multipart/alternative"
} else {
"text/html"
};
try_write_header(
&mut raw,
"Content-Type",
&format!(
"multipart/related; type=\"{root_type}\"; boundary=\"{related_boundary}\""
),
)?;
raw.extend_from_slice(b"\r\n");
write_boundary(&mut raw, &related_boundary, false);
if has_text && has_html {
let alt_boundary = new_boundary(&encapsulated_content, &mut used_boundaries);
try_write_header(
&mut raw,
"Content-Type",
&format!("multipart/alternative; boundary=\"{alt_boundary}\""),
)?;
raw.extend_from_slice(b"\r\n");
write_boundary(&mut raw, &alt_boundary, false);
write_text_part(
&mut raw,
email.body_text.as_deref().unwrap_or(""),
"text/plain",
)?;
write_boundary(&mut raw, &alt_boundary, false);
write_text_part(
&mut raw,
email.body_html.as_deref().unwrap_or(""),
"text/html",
)?;
write_boundary(&mut raw, &alt_boundary, true);
} else {
write_text_part(
&mut raw,
email.body_html.as_deref().unwrap_or(""),
"text/html",
)?;
}
for attachment in &inline_atts {
write_boundary(&mut raw, &related_boundary, false);
write_attachment_part(&mut raw, attachment)?;
}
write_boundary(&mut raw, &related_boundary, true);
} else {
if has_text && has_html {
let alt_boundary = new_boundary(&encapsulated_content, &mut used_boundaries);
try_write_header(
&mut raw,
"Content-Type",
&format!("multipart/alternative; boundary=\"{alt_boundary}\""),
)?;
raw.extend_from_slice(b"\r\n");
write_boundary(&mut raw, &alt_boundary, false);
write_text_part(
&mut raw,
email.body_text.as_deref().unwrap_or(""),
"text/plain",
)?;
write_boundary(&mut raw, &alt_boundary, false);
write_text_part(
&mut raw,
email.body_html.as_deref().unwrap_or(""),
"text/html",
)?;
write_boundary(&mut raw, &alt_boundary, true);
} else if has_text {
write_text_part(
&mut raw,
email.body_text.as_deref().unwrap_or(""),
"text/plain",
)?;
} else if has_html {
write_text_part(
&mut raw,
email.body_html.as_deref().unwrap_or(""),
"text/html",
)?;
} else {
write_text_part(&mut raw, "", "text/plain")?;
}
}
if let Some(ref mixed_b) = mixed_boundary {
for attachment in ®ular_atts {
write_boundary(&mut raw, mixed_b, false);
write_attachment_part(&mut raw, attachment)?;
}
write_boundary(&mut raw, mixed_b, true);
}
} else if has_text && has_html {
let alt_boundary = new_boundary(&encapsulated_content, &mut used_boundaries);
try_write_header(
&mut raw,
"Content-Type",
&format!("multipart/alternative; boundary=\"{alt_boundary}\""),
)?;
raw.extend_from_slice(b"\r\n");
write_boundary(&mut raw, &alt_boundary, false);
write_text_part(
&mut raw,
email.body_text.as_deref().unwrap_or(""),
"text/plain",
)?;
write_boundary(&mut raw, &alt_boundary, false);
write_text_part(
&mut raw,
email.body_html.as_deref().unwrap_or(""),
"text/html",
)?;
write_boundary(&mut raw, &alt_boundary, true);
} else if has_html {
write_text_part(
&mut raw,
email.body_html.as_deref().unwrap_or(""),
"text/html",
)?;
} else {
write_text_part(
&mut raw,
email.body_text.as_deref().unwrap_or(""),
"text/plain",
)?;
}
let mut envelope_recipients: Vec<String> = email.to.iter().map(|a| a.email.clone()).collect();
envelope_recipients.extend(email.cc.iter().map(|a| a.email.clone()));
envelope_recipients.extend(email.bcc.iter().map(|a| a.email.clone()));
Ok(BuiltMessage {
raw,
envelope_recipients,
message_id,
})
}