niax / rust-email

Implementation of MIME Messages for Rust
MIT License
63 stars 34 forks source link

as_string() produces empty line in header that can cause MTA rejection #59

Open Hocuri opened 3 years ago

Hocuri commented 3 years ago

Under some circumstances a the header in the final email contains an empty line. We are building an email client, and some messages are rejected because of this: https://github.com/deltachat/deltachat-core-rust/issues/2118

This test reproduces the problem:

#[test] fn test_no_empty_directly() { let to_tuples = vec![ ("Nnnn", "nnn@ttttttttt.de"), ("😀 ttttttt", "ttttttt@rrrrrr.net"), ("dididididididi", "t@iiiiiii.org"), ("Ttttttt", "oooooooooo@abcd.de"), ("Mmmmm", "mmmmm@rrrrrr.net"), ("Zzzzzz", "rrrrrrrrrrrrr@ttttttttt.net"), ("Xyz", "qqqqqqqqqq@rrrrrr.net"), ("", "geug@ttttttttt.de"), ("qqqqqq", "q@iiiiiii.org"), ("bbbb", "bbbb@iiiiiii.org"), ("", "fsfs@iiiiiii.org"), ("rqrqrqrqr", "rqrqr@iiiiiii.org"), ("tttttttt", "tttttttt@iiiiiii.org"), ("", "tttttt@rrrrrr.net"), ]; let mut to = Vec::new(); for (name, addr) in to_tuples { if name.is_empty() { to.push(Address::new_mailbox(addr.to_string())); } else { to.push(Address::new_mailbox_with_name( name.to_string(), addr.to_string(), )); } } let mut message = email::MimeMessage::new_blank_message(); message.headers.insert( ( "Content-Type".to_string(), "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), ) .into(), ); message .headers .insert(Header::new_with_value("To".into(), to).unwrap()); message.body = "Hi".to_string(); println!("======= HEADERS BEFORE CALL TO AS_STRING: ======="); for h in message.headers.iter() { println!("{}", h); } let msg = message.as_string(); // <-- seems like here the empty line is introduced let header_end = msg.find("Hi").unwrap(); let headers = msg[0..header_end].trim(); println!( "======= HEADERS AFTER CALL TO AS_STRING: =======\n{}\n", headers ); assert!(!headers.lines().any(|l| l.trim().is_empty())); // <-- panics }

Output:

``` ======= HEADERS BEFORE CALL TO AS_STRING: ======= Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no To: Nnnn , =?utf-8?q?=F0=9F=98=80_ttttttt?= , dididididididi , Ttttttt , Mmmmm , Zzzzzz , Xyz , , qqqqqq , bbbb , , rqrqrqrqr , tttttttt , ======= HEADERS AFTER CALL TO AS_STRING: ======= Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no To: Nnnn , =?utf-8?q?=F0=9F=98=80_ttttttt?= , dididididididi , Ttttttt , Mmmmm , Zzzzzz , Xyz , , qqqqqq , bbbb , , rqrqrqrqr , tttttttt , thread 'mimefactory::tests::test_no_empty_directly' panicked at 'assertion failed: !headers.lines().any(|l| l.trim().is_empty())', src/mimefactory.rs:1649:9 ```
Hocuri commented 3 years ago

I just realized that the test doesn't correctly reproduce the problem in the newest version, but it's still there. Here is a test that should work:

#[test]
fn test_emit_no_empty_lines() {
    // Please don't change anything to the intendation:
    let header = "To: Nnnn <nnn@ttttttttt.de>, 
=?utf-8?q?=F0=9F=98=80_ttttttt?= <ttttttt@rrrrrr.net>, 
dididididididi <t@iiiiiii.org>, Ttttttt <oooooooooo@abcd.de>, 
Mmmmm <mmmmm@rrrrrr.net>, Zzzzzz <rrrrrrrrrrrrr@ttttttttt.net>, 
Xyz <qqqqqqqqqq@rrrrrr.net>, <geug@ttttttttt.de>, qqqqqq <q@iiiiiii.org>, 
bbbb <bbbb@iiiiiii.org>, <fsfs@iiiiiii.org>, rqrqrqrqr <rqrqr@iiiiiii.org>, 
tttttttt <tttttttt@iiiiiii.org>, <tttttt@rrrrrr.net>";

    let mut builder = Rfc5322Builder::new();

    builder.emit_folded(&header.to_string()[..]);
    builder.emit_raw("\r\n");
    builder.emit_raw("\r\n");

    let res = builder.result().trim();
    println!("{}", res);
    assert!(!res.lines().any(|l| l.trim().is_empty())); // <--  panics
}