//! # format=flowed support. //! //! Format=flowed is defined in //! [RFC 3676](https://tools.ietf.org/html/rfc3676). //! //! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used //! during formatting, i.e., DelSp parameter introduced in RFC 3676 //! is assumed to be set to "no". //! //! For received messages, DelSp parameter is honoured. /// Wraps line to 72 characters using format=flowed soft breaks. /// /// 72 characters is the limit recommended by RFC 3676. /// /// The function breaks line only after SP and before non-whitespace /// characters. It also does not insert breaks before ">" to avoid the /// need to do space stuffing (see RFC 3676) for quotes. /// /// If there are long words, line may still exceed the limits on line /// length. However, this should be rare and should not result in /// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters, /// and Spam Assassin limit is 78 characters. fn format_line_flowed(line: &str, prefix: &str) -> String { let mut result = String::new(); let mut buffer = prefix.to_string(); let mut after_space = false; for c in line.chars() { if c == ' ' { buffer.push(c); after_space = true; } else if c == '>' { if buffer.is_empty() { // Space stuffing, see RFC 3676 buffer.push(' '); } buffer.push(c); after_space = false; } else { if after_space && buffer.len() >= 72 && !c.is_whitespace() { // Flush the buffer and insert soft break (SP CRLF). result += &buffer; result += "\r\n"; buffer = prefix.to_string(); } buffer.push(c); after_space = false; } } result + &buffer } /// Returns text formatted according to RFC 3767 (format=flowed). /// /// This function accepts text separated by LF, but returns text /// separated by CRLF. /// /// RFC 2646 technique is used to insert soft line breaks, so DelSp /// SHOULD be set to "no" when sending. pub(crate) fn format_flowed(text: &str) -> String { let mut result = String::new(); for line in text.split('\n') { if !result.is_empty() { result += "\r\n"; } let line_no_prefix = line.strip_prefix('>'); let is_quote = line_no_prefix.is_some(); let line = line_no_prefix.unwrap_or(line).trim(); let prefix = if is_quote { "> " } else { "" }; if prefix.len() + line.len() > 78 { result += &format_line_flowed(line, prefix); } else { result += prefix; if prefix.is_empty() && line.starts_with('>') { // Space stuffing, see RFC 3676 result.push(' '); } result += line; } } result } /// Same as format_flowed(), but adds "> " prefix to each line. pub fn format_flowed_quote(text: &str) -> String { let mut result = String::new(); for line in text.split('\n') { if !result.is_empty() { result += "\n"; } result += "> "; result += line; } format_flowed(&result) } /// Joins lines in format=flowed text. /// /// Lines must be separated by single LF. /// /// Quote processing is not supported, it is assumed that they are /// deleted during simplification. /// /// Signature separator line is not processed here, it is assumed to /// be stripped beforehand. pub fn unformat_flowed(text: &str, delsp: bool) -> String { let mut result = String::new(); let mut skip_newline = true; for line in text.split('\n') { // Revert space-stuffing let line = line.strip_prefix(' ').unwrap_or(line); if !skip_newline { result.push('\n'); } if let Some(line) = line.strip_suffix(' ') { // Flowed line result += line; if !delsp { result.push(' '); } skip_newline = true; } else { // Fixed line result += line; skip_newline = false; } } result } #[cfg(test)] mod tests { use super::*; use crate::test_utils::TestContext; #[test] fn test_format_flowed() { let text = "Foo bar baz"; assert_eq!(format_flowed(text), "Foo bar baz"); let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\ \n\ To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."; let expected = "This is the Autocrypt Setup Message used to transfer your key between clients.\r\n\ \r\n\ To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ client and enter the setup code presented on the generating device."; assert_eq!(format_flowed(text), expected); let text = "> A quote"; assert_eq!(format_flowed(text), "> A quote"); // Test space stuffing of wrapped lines let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\ > \n\ > To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."; let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\ > clients.\r\n\ > \r\n\ > To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ > client and enter the setup code presented on the generating device."; assert_eq!(format_flowed(text), expected); } #[test] fn test_unformat_flowed() { let text = "this is a very long message that should be wrapped using format=flowed and \n\ unwrapped on the receiver"; let expected = "this is a very long message that should be wrapped using format=flowed and \ unwrapped on the receiver"; assert_eq!(unformat_flowed(text, false), expected); } #[test] fn test_format_flowed_quote() { let quote = "this is a quoted line"; let expected = "> this is a quoted line"; assert_eq!(format_flowed_quote(quote), expected); let quote = "first quoted line\nsecond quoted line"; let expected = "> first quoted line\r\n> second quoted line"; assert_eq!(format_flowed_quote(quote), expected); let quote = "> foo bar baz"; let expected = "> > foo bar baz"; assert_eq!(format_flowed_quote(quote), expected); let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver"; let expected = "> this is a very long quote that should be wrapped using format=flowed and \r\n\ > unwrapped on the receiver"; assert_eq!(format_flowed_quote(quote), expected); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_quotes() -> anyhow::Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; let chat = alice.create_chat(&bob).await; let sent = alice.send_text(chat.id, "> First quote").await; let received = bob.recv_msg(&sent).await; assert_eq!(received.text.as_deref(), Some("> First quote")); assert!(received.quoted_text().is_none()); assert!(received.quoted_message(&bob).await?.is_none()); let sent = alice.send_text(chat.id, "> Second quote").await; let received = bob.recv_msg(&sent).await; assert_eq!(received.text.as_deref(), Some("> Second quote")); assert!(received.quoted_text().is_none()); assert!(received.quoted_message(&bob).await?.is_none()); Ok(()) } }