CowMailError in headers mod and use mailparse::parse_mail implementation
This commit is contained in:
@@ -1,56 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build # Build the library
|
|
||||||
cargo build --release # Build with optimizations (LTO enabled)
|
|
||||||
cargo test # Run all tests
|
|
||||||
cargo test test_name # Run a specific test
|
|
||||||
cargo doc --open # Generate and view documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
This is a Rust library for parsing, modifying, and serializing email messages (RFC 5322/MIME). It uses copy-on-write semantics to minimize allocations when working with email data.
|
|
||||||
|
|
||||||
### Core Types
|
|
||||||
|
|
||||||
- **`CowMail<H>`** (`lib.rs`): Main email message struct. Holds headers, body bytes, and nested subparts for multipart messages. Uses `Cow<[u8]>` for zero-copy parsing with owned modifications.
|
|
||||||
|
|
||||||
- **`CowMailReader`** (`lib.rs`): Streaming reader implementing `std::io::Read`. Allows efficient serialization without full memory allocation.
|
|
||||||
|
|
||||||
- **`CowHeader`** (`header.rs`): Single email header with copy-on-write storage. Tracks byte indices for key/value positions within raw data.
|
|
||||||
|
|
||||||
- **`Header` trait** (`header.rs`): Interface for header implementations including parsing, serialization, and RFC 2045 parameter handling.
|
|
||||||
|
|
||||||
- **`HeaderMap` trait** (`headers.rs`): Collection operations for headers - find, iterate, add, replace, remove.
|
|
||||||
|
|
||||||
- **`Mail` trait** (`lib.rs`): High-level operations for email bodies - content decoding, content-type detection, multipart handling.
|
|
||||||
|
|
||||||
- **`CowMailError`** (`lib.rs`): Error type for mail operations (generic errors, parse errors, missing headers).
|
|
||||||
|
|
||||||
- **`BodyType`** (`lib.rs`): Enum for body type selection (HTML, PLAIN).
|
|
||||||
|
|
||||||
- **`MessageType`** (`lib.rs`): Enum distinguishing SinglePart from MultiPart messages (with boundary).
|
|
||||||
|
|
||||||
- **`ContentType`** (`lib.rs`): Wrapper around `ParsedContentType` with conditional defaults for multipart/digest.
|
|
||||||
|
|
||||||
### Key Design Patterns
|
|
||||||
|
|
||||||
**Copy-on-Write**: Parsed messages borrow from the original byte slice. Only modified fields become owned. The `serialize()` method returns `Vec<Cow<[u8]>>` to preserve this optimization.
|
|
||||||
|
|
||||||
**Multipart Handling**: `CowMail` recursively contains `subparts: Vec<CowMail>`. Methods like `make_mixed()`, `make_alternative()`, and `make_related()` transform single-part messages into multipart containers. The original content becomes the first subpart.
|
|
||||||
|
|
||||||
**Body Part Navigation**: `get_body_part(BodyType)` traverses the MIME tree to find text/plain or text/html parts, correctly handling `multipart/alternative`, `multipart/related` with `start` parameter, and attachments. The internal `BodyStatus` enum tracks whether a part is a body, not a body, or needs traversal.
|
|
||||||
|
|
||||||
### Content Encoding
|
|
||||||
|
|
||||||
Body content is automatically encoded/decoded using the `lettre` and `mailparse` crates. `set_content()` automatically selects transfer encoding (quoted-printable for UTF-8 text) and sets appropriate headers.
|
|
||||||
|
|
||||||
### High-Level Body Manipulation
|
|
||||||
|
|
||||||
- `set_text_body()` / `set_html_body()` / `set_body()`: Intelligently insert or update body parts, creating `multipart/alternative` structures as needed
|
|
||||||
- These methods use `get_body_parent_part_mut()` to find the correct insertion point in nested MIME structures
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::{Header, parse_header};
|
use super::{Header, parse_header};
|
||||||
|
use crate::CowMailError;
|
||||||
|
|
||||||
pub trait HeaderMap<'a, H: Header<'a> + 'a> {
|
pub trait HeaderMap<'a, H: Header<'a> + 'a> {
|
||||||
fn iter_headers(&'a self, key: &str) -> impl Iterator<Item = &'a H>;
|
fn iter_headers(&'a self, key: &str) -> impl Iterator<Item = &'a H>;
|
||||||
@@ -93,7 +94,7 @@ impl<'a, H: Header<'a> + 'a> HeaderMap<'a, H> for Vec<H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn parse_headers<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(Vec<H>, usize), &'static str> {
|
pub fn parse_headers<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(Vec<H>, usize), CowMailError> {
|
||||||
let mut headers: Vec<H> = Vec::new();
|
let mut headers: Vec<H> = Vec::new();
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
loop {
|
loop {
|
||||||
@@ -107,7 +108,9 @@ pub fn parse_headers<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(Vec<H>, u
|
|||||||
ix += 2;
|
ix += 2;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
return Err("Headers were followed by an unexpected lone CR character!");
|
return Err(CowMailError::Generic(
|
||||||
|
"Headers were followed by an unexpected lone CR character!".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (header, ix_next) = parse_header(&raw_data[ix..])?;
|
let (header, ix_next) = parse_header(&raw_data[ix..])?;
|
||||||
|
|||||||
+8
-5
@@ -11,6 +11,8 @@ use std::fmt::{self, Display};
|
|||||||
use charset::decode_latin1;
|
use charset::decode_latin1;
|
||||||
use mailparse::parse_content_type;
|
use mailparse::parse_content_type;
|
||||||
|
|
||||||
|
use crate::CowMailError;
|
||||||
|
|
||||||
pub trait Header<'a>: Clone + Display {
|
pub trait Header<'a>: Clone + Display {
|
||||||
fn new<K: ToString, V: ToString>(key: K, value: V) -> Self;
|
fn new<K: ToString, V: ToString>(key: K, value: V) -> Self;
|
||||||
fn from(raw_bytes: &'a [u8], key_end: usize, value_start: usize, value_end: usize) -> Self;
|
fn from(raw_bytes: &'a [u8], key_end: usize, value_start: usize, value_end: usize) -> Self;
|
||||||
@@ -228,11 +230,11 @@ enum HeaderParseState {
|
|||||||
ValueNewline,
|
ValueNewline,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_header<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(H, usize), &'static str> {
|
pub fn parse_header<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(H, usize), CowMailError> {
|
||||||
let mut it = raw_data.iter();
|
let mut it = raw_data.iter();
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
let mut c = match it.next() {
|
let mut c = match it.next() {
|
||||||
None => return Err("Empty string provided"),
|
None => return Err(CowMailError::Generic("Empty string provided".to_string())),
|
||||||
Some(v) => *v,
|
Some(v) => *v,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,9 +247,10 @@ pub fn parse_header<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(H, usize),
|
|||||||
match state {
|
match state {
|
||||||
HeaderParseState::Initial => {
|
HeaderParseState::Initial => {
|
||||||
if c == b' ' {
|
if c == b' ' {
|
||||||
return Err(
|
return Err(CowMailError::Generic(
|
||||||
"Header cannot start with a space; it is likely an overhanging line from a previous header",
|
"Header cannot start with a space; it is likely an overhanging line from a previous header"
|
||||||
);
|
.to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
state = HeaderParseState::Key;
|
state = HeaderParseState::Key;
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+86
-77
@@ -11,7 +11,7 @@ use header::CowHeader;
|
|||||||
pub use header::{Header, HeaderMap, parse_headers};
|
pub use header::{Header, HeaderMap, parse_headers};
|
||||||
|
|
||||||
use mailparse::{
|
use mailparse::{
|
||||||
DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType, body::Body,
|
DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType, ParsedMail, body::Body,
|
||||||
parse_content_disposition, parse_content_type,
|
parse_content_disposition, parse_content_type,
|
||||||
};
|
};
|
||||||
pub use mediatype::{MediaType, names, values};
|
pub use mediatype::{MediaType, names, values};
|
||||||
@@ -836,7 +836,7 @@ impl<'a, H: Header<'a> + 'a> Mail<'a, H> for CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a [u8]> for CowMail<'a, CowHeader<'a>> {
|
impl<'a> TryFrom<&'a [u8]> for CowMail<'a, CowHeader<'a>> {
|
||||||
type Error = &'static str;
|
type Error = CowMailError;
|
||||||
|
|
||||||
fn try_from(raw_data: &'a [u8]) -> Result<Self, Self::Error> {
|
fn try_from(raw_data: &'a [u8]) -> Result<Self, Self::Error> {
|
||||||
parse_mail(raw_data)
|
parse_mail(raw_data)
|
||||||
@@ -897,14 +897,44 @@ impl std::io::Read for CowMailReader<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_mail<'a, H: Header<'a> + 'a>(raw_data: &'a [u8]) -> Result<CowMail<'a, H>, &'static str> {
|
pub fn parse_mail<'a, H: Header<'a> + 'a>(raw_data: &'a [u8]) -> Result<CowMail<'a, H>, CowMailError> {
|
||||||
parse_mail_recursive(raw_data, false)
|
let parsed = mailparse::parse_mail(raw_data)?;
|
||||||
|
build_mail(raw_data, &parsed, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_mail_recursive<'a, H: Header<'a> + 'a>(
|
/// Byte offset of `inner` within `outer`. Both have to point into the same
|
||||||
|
/// buffer, which holds for every `ParsedMail::raw_bytes` relative to its
|
||||||
|
/// parent's. Returns `None` instead of panicking, so that a mailparse that
|
||||||
|
/// stops handing back subslices surfaces as an error, not as a bad index.
|
||||||
|
fn offset_in(outer: &[u8], inner: &[u8]) -> Option<usize> {
|
||||||
|
let ix = (inner.as_ptr() as usize).checked_sub(outer.as_ptr() as usize)?;
|
||||||
|
(ix + inner.len() <= outer.len()).then_some(ix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undo mailparse's `strip_trailing_crlf`: widen `end` over the newline that
|
||||||
|
/// separates a part from the delimiter line following it, so that the slice
|
||||||
|
/// covers everything the part occupied in the message.
|
||||||
|
fn extend_over_newline(raw_data: &[u8], end: usize) -> usize {
|
||||||
|
if raw_data[end..].starts_with(b"\r\n") {
|
||||||
|
end + 2
|
||||||
|
} else if raw_data[end..].starts_with(b"\n") {
|
||||||
|
end + 1
|
||||||
|
} else {
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `CowMail` from a part mailparse has already split out.
|
||||||
|
///
|
||||||
|
/// `raw_data` is the part as it appears in the message, including the newline
|
||||||
|
/// mailparse strips off before a delimiter line; `parsed` has to start at the
|
||||||
|
/// same offset. mailparse decides where the parts are, we keep the separators
|
||||||
|
/// it drops.
|
||||||
|
fn build_mail<'a, H: Header<'a> + 'a>(
|
||||||
raw_data: &'a [u8],
|
raw_data: &'a [u8],
|
||||||
|
parsed: &ParsedMail<'a>,
|
||||||
in_multipart_digest: bool,
|
in_multipart_digest: bool,
|
||||||
) -> Result<CowMail<'a, H>, &'static str> {
|
) -> Result<CowMail<'a, H>, CowMailError> {
|
||||||
let (headers, body_idx) = parse_headers::<H>(raw_data)?;
|
let (headers, body_idx) = parse_headers::<H>(raw_data)?;
|
||||||
|
|
||||||
let mut body_end_idx = raw_data.len();
|
let mut body_end_idx = raw_data.len();
|
||||||
@@ -915,13 +945,6 @@ fn parse_mail_recursive<'a, H: Header<'a> + 'a>(
|
|||||||
end_bytes = b"\r\n";
|
end_bytes = b"\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
let default_ctype = ContentType::default_conditional(in_multipart_digest);
|
|
||||||
|
|
||||||
let ctype = headers
|
|
||||||
.find_value("Content-Type")
|
|
||||||
.map(|s| ContentType::from(s.as_str()))
|
|
||||||
.unwrap_or(default_ctype.clone());
|
|
||||||
|
|
||||||
let mut mail = CowMail {
|
let mut mail = CowMail {
|
||||||
bytes_orig: raw_data,
|
bytes_orig: raw_data,
|
||||||
header_bytes_orig: &raw_data[0..body_idx],
|
header_bytes_orig: &raw_data[0..body_idx],
|
||||||
@@ -930,73 +953,59 @@ fn parse_mail_recursive<'a, H: Header<'a> + 'a>(
|
|||||||
body_bytes_orig: &raw_data[body_idx..body_end_idx],
|
body_bytes_orig: &raw_data[body_idx..body_end_idx],
|
||||||
subparts: Vec::new(),
|
subparts: Vec::new(),
|
||||||
end_bytes,
|
end_bytes,
|
||||||
default_ctype,
|
default_ctype: ContentType::default_conditional(in_multipart_digest),
|
||||||
};
|
};
|
||||||
|
|
||||||
if ctype.mimetype.starts_with("multipart/") && ctype.params.contains_key("boundary") && raw_data.len() > body_idx {
|
// An empty subpart list covers both "not a multipart" and "boundary declared
|
||||||
let in_multipart_digest = ctype.mimetype == "multipart/digest";
|
// but never seen"; either way the body stays what it is above. Note that
|
||||||
let boundary = String::from("--") + &ctype.params["boundary"];
|
// mailparse drops the body in the latter case, so its body_bytes is of no use
|
||||||
if let Some(body_end_idx) = find_from_u8_line_prefix(raw_data, body_idx, boundary.as_bytes()) {
|
// to us here.
|
||||||
mail.body_bytes = Cow::Borrowed(&raw_data[body_idx..body_end_idx]);
|
let Some(first_part) = parsed.subparts.first() else {
|
||||||
let mut boundary_end_idx = body_end_idx + boundary.len();
|
return Ok(mail);
|
||||||
while let Some(part_start_idx) = find_from_u8(raw_data, boundary_end_idx, b"\n").map(|v| v + 1) {
|
};
|
||||||
let part_end_idx =
|
|
||||||
find_from_u8_line_prefix(raw_data, part_start_idx, boundary.as_bytes()).unwrap_or(raw_data.len());
|
let boundary = parsed
|
||||||
mail.subparts.push(parse_mail_recursive(
|
.ctype
|
||||||
&raw_data[part_start_idx..part_end_idx],
|
.params
|
||||||
in_multipart_digest,
|
.get("boundary")
|
||||||
)?);
|
.ok_or_else(|| CowMailError::Generic("multipart without a boundary parameter".to_string()))?;
|
||||||
boundary_end_idx = part_end_idx + boundary.len();
|
|
||||||
if boundary_end_idx + 2 > raw_data.len()
|
let offset_of = |part: &ParsedMail<'a>| {
|
||||||
|| (raw_data[boundary_end_idx] == b'-' && raw_data[boundary_end_idx + 1] == b'-')
|
offset_in(raw_data, part.raw_bytes)
|
||||||
{
|
.ok_or_else(|| CowMailError::Generic("subpart is not a slice of its parent".to_string()))
|
||||||
boundary_end_idx += 2;
|
};
|
||||||
if boundary_end_idx < raw_data.len() {
|
|
||||||
mail.end_bytes = &raw_data[boundary_end_idx..];
|
// The preamble ends where the delimiter line before the first part begins.
|
||||||
}
|
let first_part_idx = offset_of(first_part)?;
|
||||||
break;
|
let scan_end_idx = first_part_idx.saturating_sub(1).max(body_idx);
|
||||||
}
|
let mut delimiter_idx = raw_data[body_idx..scan_end_idx]
|
||||||
}
|
.iter()
|
||||||
|
.rposition(|&byte| byte == b'\n')
|
||||||
|
.map(|ix| body_idx + ix + 1)
|
||||||
|
.unwrap_or(body_idx);
|
||||||
|
|
||||||
|
mail.body_bytes = Cow::Borrowed(&raw_data[body_idx..delimiter_idx]);
|
||||||
|
mail.body_bytes_orig = &raw_data[body_idx..delimiter_idx];
|
||||||
|
|
||||||
|
let in_multipart_digest = parsed.ctype.mimetype == "multipart/digest";
|
||||||
|
|
||||||
|
for part in &parsed.subparts {
|
||||||
|
let part_idx = offset_of(part)?;
|
||||||
|
// Widening over the stripped newline is what keeps an empty part, or one
|
||||||
|
// whose body ends in a blank line, byte-exact: the last newline is the
|
||||||
|
// part's terminator, not part of the delimiter.
|
||||||
|
let part_end_idx = extend_over_newline(raw_data, part_idx + part.raw_bytes.len());
|
||||||
|
mail.subparts
|
||||||
|
.push(build_mail(&raw_data[part_idx..part_end_idx], part, in_multipart_digest)?);
|
||||||
|
// The part ends right where the next delimiter line begins.
|
||||||
|
delimiter_idx = part_end_idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Everything after the closing delimiter is the epilogue.
|
||||||
|
let closing_idx = delimiter_idx + 2 + boundary.len();
|
||||||
|
if closing_idx + 2 <= raw_data.len() && &raw_data[closing_idx..closing_idx + 2] == b"--" {
|
||||||
|
mail.end_bytes = &raw_data[closing_idx + 2..];
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(mail)
|
Ok(mail)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
|
|
||||||
assert!(!key.is_empty());
|
|
||||||
assert!(ix_start < line.len());
|
|
||||||
if line.len() < key.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let ix_end = line.len() - key.len();
|
|
||||||
if ix_start <= ix_end {
|
|
||||||
for i in ix_start..ix_end {
|
|
||||||
if line[i] == key[0] {
|
|
||||||
let mut success = true;
|
|
||||||
for j in 1..key.len() {
|
|
||||||
if line[i + j] != key[j] {
|
|
||||||
success = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if success {
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like find_from_u8, but additionally filters such that `key` is at the start
|
|
||||||
/// of a line (preceded by `\n`) or at the start of the search space.
|
|
||||||
fn find_from_u8_line_prefix(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
|
|
||||||
let mut start = ix_start;
|
|
||||||
while let Some(ix) = find_from_u8(line, start, key) {
|
|
||||||
if ix == ix_start || line[ix - 1] == b'\n' {
|
|
||||||
return Some(ix);
|
|
||||||
}
|
|
||||||
start = ix + 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|||||||
+109
@@ -654,3 +654,112 @@
|
|||||||
assert_eq!(mail.subparts[1].content_type().mimetype, "multipart/alternative");
|
assert_eq!(mail.subparts[1].content_type().mimetype, "multipart/alternative");
|
||||||
assert!(mail.subparts[2].is_attachment());
|
assert!(mail.subparts[2].is_attachment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test: the parser splits preamble, parts, terminators and epilogue the way
|
||||||
|
// serialization expects them back.
|
||||||
|
#[test]
|
||||||
|
fn test_parse_part_boundaries() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"bound\"\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"preamble\r\n",
|
||||||
|
"--bound\r\n",
|
||||||
|
"Content-Type: text/plain\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"Plain text\r\n",
|
||||||
|
"--bound\r\n",
|
||||||
|
"Content-Type: text/html\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"<p>HTML</p>\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--bound--\r\n",
|
||||||
|
"epilogue\r\n"
|
||||||
|
);
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(mail.raw_body(), b"preamble\r\n");
|
||||||
|
assert_eq!(mail.end_bytes, b"\r\nepilogue\r\n");
|
||||||
|
assert_eq!(mail.subparts.len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(mail.subparts[0].raw_body(), b"Plain text");
|
||||||
|
assert_eq!(mail.subparts[0].end_bytes, b"\r\n");
|
||||||
|
// A body ending in a blank line keeps both CRLFs: one for the body, one
|
||||||
|
// as the part terminator.
|
||||||
|
assert_eq!(mail.subparts[1].raw_body(), b"<p>HTML</p>\r\n");
|
||||||
|
assert_eq!(mail.subparts[1].end_bytes, b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: an empty part keeps its terminator instead of swallowing the header
|
||||||
|
// separator.
|
||||||
|
#[test]
|
||||||
|
fn test_parse_empty_part() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"bound\"\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--bound\r\n",
|
||||||
|
"Subject: empty\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--bound--\r\n"
|
||||||
|
);
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(mail.subparts.len(), 1);
|
||||||
|
assert_eq!(mail.subparts[0].bytes_orig, b"Subject: empty\r\n\r\n");
|
||||||
|
assert_eq!(mail.subparts[0].raw_body(), b"");
|
||||||
|
assert_eq!(mail.subparts[0].end_bytes, b"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: a declared boundary that never appears must not lose the body
|
||||||
|
// (mailparse drops it in that case).
|
||||||
|
#[test]
|
||||||
|
fn test_parse_missing_boundary_keeps_body() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"bound\"\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"no boundary here\r\n"
|
||||||
|
);
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
assert!(mail.subparts.is_empty());
|
||||||
|
assert_eq!(mail.raw_body(), b"no boundary here");
|
||||||
|
assert_eq!(mail.end_bytes, b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: a part of a multipart/digest defaults to message/rfc822
|
||||||
|
#[test]
|
||||||
|
fn test_parse_digest_default_ctype() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: multipart/digest; boundary=\"bound\"\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--bound\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"Subject: forwarded\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--bound--\r\n"
|
||||||
|
);
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(mail.subparts.len(), 1);
|
||||||
|
assert_eq!(mail.subparts[0].content_type().mimetype, "message/rfc822");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: a closing delimiter at the very end of the buffer, with no trailing
|
||||||
|
// CRLF, used to be missed by the hand-rolled boundary search.
|
||||||
|
#[test]
|
||||||
|
fn test_parse_closing_boundary_at_eof() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"bound\"\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--bound\r\n",
|
||||||
|
"Content-Type: text/plain\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"Plain text\r\n",
|
||||||
|
"--bound--"
|
||||||
|
);
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(mail.subparts.len(), 1);
|
||||||
|
assert_eq!(mail.subparts[0].raw_body(), b"Plain text");
|
||||||
|
assert_eq!(mail.subparts[0].end_bytes, b"\r\n");
|
||||||
|
assert_eq!(mail.end_bytes, b"");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user