use crate::error::DkimError;
pub fn find_body_offset(raw: &[u8]) -> Option<usize> {
let mut i = 0;
while i < raw.len() {
if i + 3 < raw.len() && &raw[i..i + 4] == b"\r\n\r\n" {
return Some(i + 4);
}
if i + 1 < raw.len() && raw[i] == b'\n' && raw[i + 1] == b'\n' {
return Some(i + 2);
}
i += 1;
}
None
}
pub fn body_offset_minus_blank(body_offset: usize, raw: &[u8]) -> usize {
if body_offset >= 2 && &raw[body_offset - 2..body_offset] == b"\r\n" {
body_offset - 2
} else if body_offset >= 1 && raw[body_offset - 1] == b'\n' {
body_offset - 1
} else {
body_offset
}
}
pub fn find_header_value_in_raw(headers: &[u8], name: &[u8]) -> Result<String, DkimError> {
let mut i = 0;
while i < headers.len() {
if i + name.len() < headers.len()
&& headers[i..i + name.len()].eq_ignore_ascii_case(name)
&& headers[i + name.len()] == b':'
{
let value_start = i + name.len() + 1;
let mut j = value_start;
while j < headers.len() {
if headers[j] == b'\n' {
let after = j + 1;
if after < headers.len() && matches!(headers[after], b' ' | b'\t') {
j += 1;
continue;
}
return Ok(String::from_utf8_lossy(&headers[value_start..j]).into_owned());
}
j += 1;
}
return Ok(String::from_utf8_lossy(&headers[value_start..j]).into_owned());
}
while i < headers.len() && headers[i] != b'\n' {
i += 1;
}
i += 1;
}
Err(DkimError::MissingHeader)
}
pub fn find_all_header_values_in_raw(headers: &[u8], name: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while i < headers.len() {
if i + name.len() < headers.len()
&& headers[i..i + name.len()].eq_ignore_ascii_case(name)
&& headers[i + name.len()] == b':'
{
let value_start = i + name.len() + 1;
let mut j = value_start;
while j < headers.len() {
if headers[j] == b'\n' {
let after = j + 1;
if after < headers.len() && matches!(headers[after], b' ' | b'\t') {
j += 1;
continue;
}
out.push(String::from_utf8_lossy(&headers[value_start..j]).into_owned());
i = j;
break;
}
j += 1;
}
if j >= headers.len() {
out.push(String::from_utf8_lossy(&headers[value_start..j]).into_owned());
return out;
}
}
while i < headers.len() && headers[i] != b'\n' {
i += 1;
}
i += 1;
}
out
}
pub fn find_header_value<'a>(headers: &'a [u8], name: &str) -> Option<&'a str> {
let bytes = name.as_bytes();
let mut i = 0;
while i < headers.len() {
if i + bytes.len() < headers.len()
&& headers[i..i + bytes.len()].eq_ignore_ascii_case(bytes)
&& headers[i + bytes.len()] == b':'
{
let value_start = i + bytes.len() + 1;
let mut j = value_start;
while j < headers.len() {
if headers[j] == b'\n' {
let after = j + 1;
if after < headers.len() && matches!(headers[after], b' ' | b'\t') {
j += 1;
continue;
}
let end = if j > value_start && headers[j - 1] == b'\r' {
j - 1
} else {
j
};
return std::str::from_utf8(&headers[value_start..end]).ok();
}
j += 1;
}
return std::str::from_utf8(&headers[value_start..j]).ok();
}
while i < headers.len() && headers[i] != b'\n' {
i += 1;
}
i += 1;
}
None
}
pub fn collect_signed_headers(headers_raw: &[u8], names: &[String]) -> Vec<(String, Option<String>)> {
let mut occurrences: Vec<(String, String)> = Vec::new();
let mut i = 0;
while i < headers_raw.len() {
let line_start = i;
let mut colon: Option<usize> = None;
while i < headers_raw.len() && headers_raw[i] != b'\n' {
if headers_raw[i] == b':' && colon.is_none() {
colon = Some(i);
}
i += 1;
}
let value_end_first = if i > line_start && headers_raw[i.saturating_sub(1)] == b'\r' {
i - 1
} else {
i
};
if i < headers_raw.len() {
i += 1; }
if let Some(colon_pos) = colon {
let mut value_end = value_end_first;
while i < headers_raw.len() && matches!(headers_raw[i], b' ' | b'\t') {
while i < headers_raw.len() && headers_raw[i] != b'\n' {
i += 1;
}
value_end = if i > line_start && headers_raw[i.saturating_sub(1)] == b'\r' {
i - 1
} else {
i
};
if i < headers_raw.len() {
i += 1;
}
}
let name = std::str::from_utf8(&headers_raw[line_start..colon_pos])
.unwrap_or("")
.to_ascii_lowercase();
let value = std::str::from_utf8(&headers_raw[colon_pos + 1..value_end])
.unwrap_or("")
.to_string();
occurrences.push((name, value));
}
}
let mut consumed = vec![false; occurrences.len()];
let mut result = Vec::with_capacity(names.len());
for name in names {
let name_lower = name.to_ascii_lowercase();
let mut found: Option<String> = None;
for idx in (0..occurrences.len()).rev() {
if !consumed[idx] && occurrences[idx].0 == name_lower {
consumed[idx] = true;
found = Some(occurrences[idx].1.clone());
break;
}
}
result.push((name.clone(), found));
}
result
}
pub fn clear_b_value(value: &str) -> String {
let bytes = value.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let is_b_start = if i + 1 < bytes.len() && bytes[i] == b'b' && bytes[i + 1] == b'=' {
let mut k = i;
while k > 0 {
k -= 1;
if !matches!(bytes[k], b' ' | b'\t' | b'\r' | b'\n') {
break;
}
}
k == 0 || bytes[k] == b';'
} else {
false
};
if is_b_start {
out.extend_from_slice(b"b=");
i += 2;
while i < bytes.len() && bytes[i] != b';' {
i += 1;
}
continue;
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn body_offset_simple_crlf_crlf() {
let raw = b"From: a@b\r\n\r\nbody";
let off = find_body_offset(raw).unwrap();
assert_eq!(&raw[off..], b"body");
}
#[test]
fn body_offset_lf_lf() {
let raw = b"From: a@b\n\nbody";
let off = find_body_offset(raw).unwrap();
assert_eq!(&raw[off..], b"body");
}
#[test]
fn find_header_extracts_value() {
let headers = b"From: alice@example.com\r\nTo: bob@example.com\r\n";
let v = find_header_value(headers, "From");
assert_eq!(v, Some(" alice@example.com"));
}
#[test]
fn find_header_handles_folded() {
let headers = b"Subject: line1\r\n line2\r\nFrom: a@b\r\n";
let v = find_header_value(headers, "Subject");
assert_eq!(v, Some(" line1\r\n line2"));
}
#[test]
fn collect_signed_headers_basic() {
let headers = b"Date: today\r\nFrom: alice@example.com\r\nTo: bob@example.com\r\n";
let names = vec!["Date".to_string(), "From".to_string(), "To".to_string()];
let got = collect_signed_headers(headers, &names);
assert_eq!(got.len(), 3);
assert_eq!(got[0], ("Date".to_string(), Some(" today".to_string())));
assert_eq!(
got[1],
("From".to_string(), Some(" alice@example.com".to_string()))
);
assert_eq!(
got[2],
("To".to_string(), Some(" bob@example.com".to_string()))
);
}
#[test]
fn collect_signed_headers_repeated_h_consumes_bottom_up() {
let headers = b"From: first@example.com\r\nFrom: second@example.com\r\nSubject: hi\r\n";
let names = vec!["From".to_string(), "From".to_string()];
let got = collect_signed_headers(headers, &names);
assert_eq!(
got[0],
("From".to_string(), Some(" second@example.com".to_string()))
);
assert_eq!(
got[1],
("From".to_string(), Some(" first@example.com".to_string()))
);
}
#[test]
fn collect_signed_headers_overcount_yields_none() {
let headers = b"Date: today\r\nFrom: alice@example.com\r\n";
let names = vec!["From".to_string(), "From".to_string()];
let got = collect_signed_headers(headers, &names);
assert_eq!(
got[0],
("From".to_string(), Some(" alice@example.com".to_string()))
);
assert_eq!(got[1], ("From".to_string(), None));
}
#[test]
fn collect_signed_headers_case_insensitive_name_match() {
let headers = b"FROM: alice@example.com\r\n";
let names = vec!["From".to_string()];
let got = collect_signed_headers(headers, &names);
assert_eq!(
got[0],
("From".to_string(), Some(" alice@example.com".to_string()))
);
}
#[test]
fn collect_signed_headers_missing_header_yields_none() {
let headers = b"From: a@b\r\n";
let names = vec!["From".to_string(), "Reply-To".to_string()];
let got = collect_signed_headers(headers, &names);
assert_eq!(got[0].1.as_deref(), Some(" a@b"));
assert_eq!(got[1].1, None);
}
#[test]
fn collect_signed_headers_folded_value() {
let headers = b"Subject: line1\r\n line2\r\n\tline3\r\nFrom: a@b\r\n";
let names = vec!["Subject".to_string()];
let got = collect_signed_headers(headers, &names);
assert_eq!(got[0].1.as_deref(), Some(" line1\r\n line2\r\n\tline3"));
}
#[test]
fn clear_b_replaces_value_only() {
let v = "i=1; a=rsa-sha256; d=ex.com; s=mail; h=From; bh=BH; b=SIG/+abc==";
assert_eq!(
clear_b_value(v),
"i=1; a=rsa-sha256; d=ex.com; s=mail; h=From; bh=BH; b="
);
}
#[test]
fn find_all_header_values_in_raw_zero_matches() {
let headers = b"From: a\r\n";
assert!(find_all_header_values_in_raw(headers, b"DKIM-Signature").is_empty());
}
#[test]
fn find_all_header_values_in_raw_single_match() {
let headers = b"DKIM-Signature: v=1; d=a.com\r\nFrom: a\r\n";
let v = find_all_header_values_in_raw(headers, b"DKIM-Signature");
assert_eq!(v.len(), 1);
assert_eq!(v[0], " v=1; d=a.com\r");
}
#[test]
fn find_all_header_values_in_raw_multi_match() {
let headers =
b"DKIM-Signature: v=1; d=a.com\r\nFrom: a\r\nDKIM-Signature: v=1; d=b.com\r\n";
let v = find_all_header_values_in_raw(headers, b"DKIM-Signature");
assert_eq!(v.len(), 2);
assert_eq!(v[0], " v=1; d=a.com\r");
assert_eq!(v[1], " v=1; d=b.com\r");
}
#[test]
fn find_all_header_values_in_raw_handles_folded() {
let headers = b"DKIM-Signature: v=1;\r\n d=a.com\r\nDKIM-Signature: v=1; d=b.com\r\n";
let v = find_all_header_values_in_raw(headers, b"DKIM-Signature");
assert_eq!(v.len(), 2);
assert!(v[0].contains("d=a.com"));
assert!(v[1].contains("d=b.com"));
}
#[test]
fn find_header_value_in_raw_returns_owned() {
let headers = b"DKIM-Signature: v=1; a=rsa-sha256\r\nFrom: a@b\r\n";
let v = find_header_value_in_raw(headers, b"DKIM-Signature").unwrap();
assert_eq!(v, " v=1; a=rsa-sha256\r");
}
}