diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 127a23111..078e608e5 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -15,6 +15,7 @@ use crate::message::{self, Message}; use crate::mimeparser::SystemMessage; use crate::param::*; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; +use crate::simplify::escape_message_footer_marks; use crate::stock::StockMessage; // attachments of 25 mb brutto should work on the majority of providers @@ -807,7 +808,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { let message_text = format!( "{}{}{}{}{}", fwdhint.unwrap_or_default(), - &final_text, + escape_message_footer_marks(final_text), if !final_text.is_empty() && !footer.is_empty() { "\r\n\r\n" } else { diff --git a/src/simplify.rs b/src/simplify.rs index cf61dc8ae..79d6f6605 100644 --- a/src/simplify.rs +++ b/src/simplify.rs @@ -1,13 +1,41 @@ +// protect lines starting with `--` against being treated as a footer. +// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B); +// this should be invisible on most systems and there is no need to unescape it again +// (which won't be done by non-deltas anyway) +// +// this escapes a bit more than actually needed by delta (eg. also lines as "-- footer"), +// but for non-delta-compatibility, that seems to be better. +// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced) +pub fn escape_message_footer_marks(text: &str) -> String { + if text.starts_with("--") { + "-\u{200B}-".to_string() + &text[2..].replace("\n--", "\n-\u{200B}-") + } else { + text.replace("\n--", "\n-\u{200B}-") + } +} + /// Remove standard (RFC 3676, ยง4.3) footer if it is found. fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] { + let mut nearly_standard_footer = None; for (ix, &line) in lines.iter().enumerate() { - // quoted-printable may encode `-- ` to `-- =20` which is converted - // back to `-- ` match line { + // some providers encode `-- ` to `-- =20` which results in `-- ` "-- " | "-- " => return &lines[..ix], + // some providers encode `-- ` to `=2D-` which results in only `--`; + // use that only when no other footer is found + // and if the line before is empty and the line after is not empty + "--" => { + if (ix == 0 || lines[ix - 1] == "") && ix != lines.len() - 1 && lines[ix + 1] != "" + { + nearly_standard_footer = Some(ix); + } + } _ => (), } } + if let Some(ix) = nearly_standard_footer { + return &lines[..ix]; + } lines } @@ -156,7 +184,8 @@ fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> if is_cut_at_end && (!is_cut_at_begin || !empty_body) { ret += " [...]"; } - ret + // redo escaping done by escape_message_footer_marks() + ret.replace("\u{200B}", "") } /** @@ -268,4 +297,46 @@ mod tests { assert_eq!(lines, &["not a quote", "> first", "> second"]); assert!(!has_top_quote); } + + #[test] + fn test_escape_message_footer_marks() { + let esc = escape_message_footer_marks("--\n--text --in line"); + assert_eq!(esc, "-\u{200B}-\n-\u{200B}-text --in line"); + + let esc = escape_message_footer_marks("--\r\n--text"); + assert_eq!(esc, "-\u{200B}-\r\n-\u{200B}-text"); + } + + #[test] + fn test_remove_message_footer() { + let input = "text\n--\nno footer".to_string(); + let (plain, _) = simplify(input, true); + assert_eq!(plain, "text\n--\nno footer"); + + let input = "text\n\n--\n\nno footer".to_string(); + let (plain, _) = simplify(input, true); + assert_eq!(plain, "text\n\n--\n\nno footer"); + + let input = "text\n\n-- no footer\n\n".to_string(); + let (plain, _) = simplify(input, true); + assert_eq!(plain, "text\n\n-- no footer"); + + let input = "text\n\n--\nno footer\n-- \nfooter".to_string(); + let (plain, _) = simplify(input, true); + assert_eq!(plain, "text\n\n--\nno footer"); + + let input = "text\n\n--\ntreated as footer when unescaped".to_string(); + let (plain, _) = simplify(input.clone(), true); + assert_eq!(plain, "text"); // see remove_message_footer() for some explanations + let escaped = escape_message_footer_marks(&input); + let (plain, _) = simplify(escaped, true); + assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped"); + + let input = "--\ntreated as footer when unescaped".to_string(); + let (plain, _) = simplify(input.clone(), true); + assert_eq!(plain, ""); // see remove_message_footer() for some explanations + let escaped = escape_message_footer_marks(&input); + let (plain, _) = simplify(escaped, true); + assert_eq!(plain, "--\ntreated as footer when unescaped"); + } }