Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,10 @@ fn normalize_search_term(value: &str) -> Option<String> {
}

fn imap_quote(value: &str) -> String {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
// RFC 3501: quoted strings must not contain CR or LF.
// Strip them before escaping to prevent IMAP command injection.
let sanitized = value.replace('\r', "").replace('\n', "");
let escaped = sanitized.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}

Expand Down Expand Up @@ -1149,4 +1152,22 @@ mod tests {
assert!(!is_gmail_imap_host("imap.mail.yahoo.com"));
assert!(!is_gmail_imap_host("imap.example.com"));
}

#[test]
fn test_imap_quote_strips_crlf_injection() {
// A payload that would inject an extra IMAP command if \r\n were not removed.
let malicious = "x\r\nA001 STORE 1 +FLAGS (\\Deleted)";
let quoted = imap_quote(malicious);
assert!(!quoted.contains('\r'), "CR must be stripped");
assert!(!quoted.contains('\n'), "LF must be stripped");
// The output should still be a valid quoted string.
assert!(quoted.starts_with('"'));
assert!(quoted.ends_with('"'));
}

#[test]
fn test_imap_quote_escapes_backslash_and_quote() {
let quoted = imap_quote(r#"say "hello" \ world"#);
assert_eq!(quoted, r#""say \"hello\" \\ world""#);
}
}
Loading