use super::{address, get_header_value, params};
use crate::types::is_valid_bare_message_id_body;
pub(crate) fn extract_message_id(headers: &[(String, String)]) -> Option<String> {
get_header_value(headers, "message-id").and_then(|v| {
if let Some(id) = extract_first_msg_id(&v) {
return Some(id);
}
let trimmed = v.trim();
if trimmed.is_empty()
|| trimmed.contains('<')
|| trimmed.contains('>')
|| !is_valid_bare_message_id_body(trimmed)
{
None
} else {
Some(trimmed.to_string())
}
})
}
pub(crate) fn extract_in_reply_to(headers: &[(String, String)]) -> Vec<String> {
headers
.iter()
.filter(|(k, _)| k == "in-reply-to")
.flat_map(|(_, v)| extract_all_msg_ids(v))
.collect()
}
pub(crate) fn extract_references(headers: &[(String, String)]) -> Vec<String> {
headers
.iter()
.filter(|(k, _)| k == "references")
.flat_map(|(_, v)| extract_all_msg_ids(v))
.collect()
}
fn extract_first_msg_id(value: &str) -> Option<String> {
let uncommented = address::strip_comments(value);
let mut offset = 0usize;
while let Some(start) = find_next_unquoted_msg_id_delim(&uncommented, offset, b'<') {
let Some(end) = find_next_unquoted_msg_id_delim(&uncommented, start + 1, b'>') else {
break;
};
let id = uncommented[start + 1..end].trim();
let normalized = normalize_obs_msg_id_body(id);
if is_valid_bare_message_id_body(&normalized) {
return Some(normalized);
}
offset = end + 1;
}
None
}
fn extract_all_msg_ids(value: &str) -> Vec<String> {
let uncommented = address::strip_comments(value);
let mut ids = Vec::new();
let mut offset = 0usize;
while let Some(start) = find_next_unquoted_msg_id_delim(&uncommented, offset, b'<') {
let before = &uncommented[offset..start];
ids.extend(extract_bare_msg_ids_segment(before));
if let Some(end) = find_next_unquoted_msg_id_delim(&uncommented, start + 1, b'>') {
let id = uncommented[start + 1..end].trim();
let normalized = normalize_obs_msg_id_body(id);
if is_valid_bare_message_id_body(&normalized) {
ids.push(normalized);
}
offset = end + 1;
} else {
ids.extend(extract_bare_msg_ids_segment(&uncommented[start + 1..]));
break;
}
}
ids.extend(extract_bare_msg_ids_segment(&uncommented[offset..]));
ids
}
fn normalize_obs_msg_id_body(value: &str) -> String {
let mut normalized = String::with_capacity(value.len());
let mut in_quotes = false;
let mut escaped = false;
for ch in value.chars() {
if in_quotes {
normalized.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_quotes = false;
}
continue;
}
match ch {
'"' => {
in_quotes = true;
normalized.push(ch);
}
' ' | '\t' => {}
_ => normalized.push(ch),
}
}
normalized
}
fn find_next_unquoted_msg_id_delim(value: &str, start: usize, target: u8) -> Option<usize> {
value
.as_bytes()
.iter()
.enumerate()
.skip(start)
.find_map(|(idx, byte)| {
(*byte == target
&& !params::is_inside_quotes(value, idx)
&& !params::is_inside_comment(value, idx))
.then_some(idx)
})
}
fn extract_bare_msg_ids_segment(segment: &str) -> Vec<String> {
segment
.split_whitespace()
.filter(|token| is_valid_bare_message_id_body(token))
.map(str::to_string)
.collect()
}