//! # 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. #![cfg_attr(not(test), forbid(clippy::indexing_slicing))] #![cfg_attr(not(test), forbid(clippy::string_slice))] /// 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 = prefix.ends_with(' '); for c in line.chars() { if c == ' ' { if buffer.is_empty() { // Space stuffing, see RFC 3676 buffer.push(' '); } 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 3676 (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 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 = line.trim_end(); let quote_depth = line.chars().take_while(|&c| c == '>').count(); let (prefix, mut line) = line.split_at(quote_depth); let mut prefix = prefix.to_string(); if quote_depth > 0 { if let Some(s) = line.strip_prefix(' ') { line = s; prefix += " "; } } result += &format_line_flowed(line, &prefix); } 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. /// /// 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') { let line = if !result.is_empty() && skip_newline { line.trim_start_matches('>') } else { line }; // 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::*; #[test] fn test_format_flowed() { let text = ""; assert_eq!(format_flowed(text), ""); let text = "Foo bar baz"; assert_eq!(format_flowed(text), text); let text = ">Foo bar"; assert_eq!(format_flowed(text), text); let text = "> Foo bar"; assert_eq!(format_flowed(text), text); let text = ">\n\nA"; assert_eq!(format_flowed(text), ">\r\n\r\nA"); 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"); let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; assert_eq!( format_flowed(text), "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A" ); // 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); 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 space stuffing of spaces. let text = " Foo bar baz"; assert_eq!(format_flowed(text), " Foo bar baz"); let text = " Foo bar baz"; assert_eq!(format_flowed(text), " Foo bar baz"); let text = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA"; let expected = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \r\nAAAAAA"; 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); let text = " Foo bar"; let expected = " Foo bar"; assert_eq!(unformat_flowed(text, false), expected); let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A"; let expected = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; 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); } }