chore: sync mailcore v0.1.0-beta.1
This commit is contained in:
140
mailcore/tests/subject_decoding_test.rs
Normal file
140
mailcore/tests/subject_decoding_test.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use mail_parser::MessageParser;
|
||||
|
||||
// Duplicate the word-joiner here since pub(crate) is not visible from integration tests.
|
||||
fn parse_encoded_word(s: &str) -> Option<(&str, &str, &str, &str)> {
|
||||
let s = s.strip_prefix("=?")?;
|
||||
let (charset, rest) = s.split_once('?')?;
|
||||
let (encoding, rest) = rest.split_once('?')?;
|
||||
if encoding.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let enc_char = encoding.chars().next()?.to_ascii_uppercase();
|
||||
if enc_char != 'B' && enc_char != 'Q' {
|
||||
return None;
|
||||
}
|
||||
let end = rest.find("?=")?;
|
||||
let payload = &rest[..end];
|
||||
let remaining = &rest[end + 2..];
|
||||
Some((charset, encoding, payload, remaining))
|
||||
}
|
||||
|
||||
fn join_adjacent_encoded_words(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut pos = 0;
|
||||
while pos < s.len() {
|
||||
let remaining = &s[pos..];
|
||||
if !remaining.starts_with("=?") {
|
||||
let ch = remaining.chars().next().unwrap();
|
||||
out.push(ch);
|
||||
pos += ch.len_utf8();
|
||||
continue;
|
||||
}
|
||||
match parse_encoded_word(remaining) {
|
||||
None => {
|
||||
out.push('=');
|
||||
pos += 1;
|
||||
}
|
||||
Some((charset, encoding, payload, after_first)) => {
|
||||
let mut combined = payload.to_string();
|
||||
let mut cursor = after_first;
|
||||
loop {
|
||||
let ws = cursor.trim_start_matches(|c: char| c == ' ' || c == '\t');
|
||||
if !ws.starts_with("=?") {
|
||||
break;
|
||||
}
|
||||
match parse_encoded_word(ws) {
|
||||
None => break,
|
||||
Some((cs2, enc2, payload2, after_next)) => {
|
||||
if cs2.eq_ignore_ascii_case(charset)
|
||||
&& enc2.eq_ignore_ascii_case(encoding)
|
||||
{
|
||||
combined.push_str(payload2);
|
||||
cursor = after_next;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_str("=?");
|
||||
out.push_str(charset);
|
||||
out.push('?');
|
||||
out.push_str(encoding);
|
||||
out.push('?');
|
||||
out.push_str(&combined);
|
||||
out.push_str("?=");
|
||||
pos += remaining.len() - cursor.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn preprocess(bytes: &[u8]) -> Vec<u8> {
|
||||
match std::str::from_utf8(bytes) {
|
||||
Ok(s) => join_adjacent_encoded_words(s).into_bytes(),
|
||||
Err(_) => bytes.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_subject(raw: &[u8]) -> String {
|
||||
let preprocessed = preprocess(raw);
|
||||
MessageParser::default()
|
||||
.parse(&preprocessed)
|
||||
.and_then(|m| m.subject().map(|s| s.to_string()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// ЕИРЦ = D0 95 D0 98 D0 A0 D0 A6
|
||||
/// Full base64: 0JXQmNCg0KY=
|
||||
#[test]
|
||||
fn verify_full_base64() {
|
||||
let raw = b"Subject: =?UTF-8?B?0JXQmNCg0KY=?=\n\nBody";
|
||||
assert_eq!(
|
||||
parse_subject(raw),
|
||||
"ЕИРЦ",
|
||||
"Full base64 sanity check failed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Plain UTF-8 – always worked.
|
||||
#[test]
|
||||
fn test_raw_utf8() {
|
||||
let raw = "Subject: ЕИРЦ\n\nBody".as_bytes();
|
||||
assert_eq!(parse_subject(raw), "ЕИРЦ");
|
||||
}
|
||||
|
||||
/// UTF-8 Quoted-Printable – already worked before fix.
|
||||
#[test]
|
||||
fn test_utf8_qp() {
|
||||
let raw = b"Subject: =?UTF-8?Q?=D0=95=D0=98=D0=A0=D0=A6?=\n\nBody";
|
||||
assert_eq!(parse_subject(raw), "ЕИРЦ");
|
||||
}
|
||||
|
||||
/// Multi-byte char split across two adjacent words (no whitespace).
|
||||
/// Split: first 6 bytes (D0 95 D0 98 D0 A0) → 0JXQmNCg
|
||||
/// last 2 bytes (D0 A6) → 0KY=
|
||||
/// mail-parser sees И split across the boundary.
|
||||
#[test]
|
||||
fn test_split_utf8_encoded_words() {
|
||||
let raw = b"Subject: =?UTF-8?B?0JXQmNCg?==?UTF-8?B?0KY=?=\n\nBody";
|
||||
assert_eq!(
|
||||
parse_subject(raw),
|
||||
"ЕИРЦ",
|
||||
"Split encoded-word (no space) failed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Same split with linear whitespace (RFC 2047 §6.2).
|
||||
#[test]
|
||||
fn test_split_utf8_with_whitespace() {
|
||||
let raw = b"Subject: =?UTF-8?B?0JXQmNCg?= =?UTF-8?B?0KY=?=\n\nBody";
|
||||
assert_eq!(parse_subject(raw), "ЕИРЦ", "Split with whitespace failed");
|
||||
}
|
||||
|
||||
/// Split across multiple lines (CRLF + space), simulating folded headers.
|
||||
#[test]
|
||||
fn test_split_utf8_folded_header() {
|
||||
let raw = b"Subject: =?UTF-8?B?0JXQmNCg?=\r\n =?UTF-8?B?0KY=?=\n\nBody";
|
||||
assert_eq!(parse_subject(raw), "ЕИРЦ", "Split folded header failed");
|
||||
}
|
||||
79
mailcore/tests/test_imap_smtp_mock.rs
Normal file
79
mailcore/tests/test_imap_smtp_mock.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use lettre::message::{MultiPart, SinglePart};
|
||||
use lettre::Message;
|
||||
use mail_parser::MimeHeaders;
|
||||
use std::error::Error;
|
||||
|
||||
/// Mock integration test validating that our SMTP serialization and IMAP parsing
|
||||
/// logic functions cleanly without dropping or ignoring attributes, executing fully headless
|
||||
/// without needing a binding live TCP/TLS external socket constraint.
|
||||
#[test]
|
||||
fn test_smtp_builder_and_imap_parser_mock() -> Result<(), Box<dyn Error>> {
|
||||
// 1. SMTP Mock Builder Simulation
|
||||
let sender = "alice@example.com";
|
||||
let receiver = "bob@example.com";
|
||||
let subject = "Mock Integration Payload";
|
||||
let body_text = "This is a body payload.";
|
||||
|
||||
// Simulate Attachments
|
||||
let content = b"FILE_CONTENT_MOCK".to_vec();
|
||||
let attachment_name = "secret.txt";
|
||||
|
||||
let email = Message::builder()
|
||||
.from(sender.parse()?)
|
||||
.to(receiver.parse()?)
|
||||
.subject(subject)
|
||||
.multipart(
|
||||
MultiPart::mixed()
|
||||
.singlepart(SinglePart::plain(body_text.to_string()))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(lettre::message::header::ContentType::parse("text/plain")?)
|
||||
.header(lettre::message::header::ContentDisposition::attachment(
|
||||
attachment_name,
|
||||
))
|
||||
.body(content.clone()),
|
||||
),
|
||||
)?;
|
||||
|
||||
let raw_email_bytes = email.formatted();
|
||||
|
||||
// 2. IMAP Mock Parsing Simulation (What we'd do on receiving bytes from IMAP FETCH)
|
||||
let parsed_message = mail_parser::MessageParser::default()
|
||||
.parse(&raw_email_bytes)
|
||||
.ok_or("Failed to parse raw email bytes")?;
|
||||
|
||||
// Validate Headers
|
||||
assert_eq!(
|
||||
parsed_message.subject().unwrap(),
|
||||
"Mock Integration Payload"
|
||||
);
|
||||
|
||||
let from_header = parsed_message.from().unwrap().first();
|
||||
assert_eq!(from_header.unwrap().address().unwrap(), "alice@example.com");
|
||||
|
||||
let to_header = parsed_message.to().unwrap().first();
|
||||
assert_eq!(to_header.unwrap().address().unwrap(), "bob@example.com");
|
||||
|
||||
// Validate Body
|
||||
assert_eq!(
|
||||
parsed_message.body_text(0).unwrap().trim(),
|
||||
"This is a body payload."
|
||||
);
|
||||
|
||||
// Validate Attachments
|
||||
let mut attachment_found = false;
|
||||
for attachment in parsed_message.attachments() {
|
||||
if let Some(name) = attachment.attachment_name() {
|
||||
if name == attachment_name {
|
||||
attachment_found = true;
|
||||
assert_eq!(attachment.contents(), content.as_slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
attachment_found,
|
||||
"Attachment was dropped during serialization/deserialization mock"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user