use std::{
io,
sync::{Arc, Mutex},
};
use crate::store::{Message, Store};
use tokio::io::AsyncWriteExt;
use super::ImapReader;
use crate::imap::fetch::header_value;
use crate::imap::util::parse_imap_args;
pub(super) async fn send_search(
writer: &mut ImapReader,
store: &Arc<Mutex<Store>>,
user: &str,
mailbox: &str,
tokens: &[String],
use_uid: bool,
) -> io::Result<()> {
let messages = store
.lock()
.expect("store lock poisoned")
.list(user, mailbox);
let mut hits = Vec::new();
for (idx, message) in messages.iter().enumerate() {
if matches_search_tokens(message, tokens) {
let id = if use_uid {
message.uid
} else {
match u32::try_from(idx + 1) {
Ok(value) => value,
Err(_) => continue,
}
};
hits.push(id.to_string());
}
}
if hits.is_empty() {
write_raw(writer, b"* SEARCH\r\n").await?;
} else {
write_raw(
writer,
format!("* SEARCH {}\r\n", hits.join(" ")).as_bytes(),
)
.await?;
}
Ok(())
}
pub(super) fn parse_search_args(line: &str) -> Vec<String> {
let mut args = parse_imap_args(line);
if args
.first()
.is_some_and(|s| s.eq_ignore_ascii_case("SEARCH"))
{
args.remove(0);
}
args
}
fn matches_search_tokens(message: &Message, tokens: &[String]) -> bool {
let mut idx = 0;
while idx < tokens.len() {
let (matched, next_idx) = evaluate_search_token(message, tokens, idx);
if !matched {
return false;
}
if next_idx <= idx {
return false;
}
idx = next_idx;
}
true
}
fn evaluate_search_token(message: &Message, tokens: &[String], idx: usize) -> (bool, usize) {
if idx >= tokens.len() {
return (true, idx);
}
let key = tokens
.get(idx)
.map(|s| s.to_ascii_uppercase())
.unwrap_or_default();
match key.as_str() {
"OR" => {
let (left, next_idx) = evaluate_search_token(message, tokens, idx + 1);
let (right, next_idx) = evaluate_search_token(message, tokens, next_idx);
(left || right, next_idx)
}
"NOT" => {
let (matched, next_idx) = evaluate_search_token(message, tokens, idx + 1);
(!matched, next_idx)
}
"ALL" => (true, idx + 1),
"SEEN" => (message.seen, idx + 1),
"UNSEEN" => (!message.seen, idx + 1),
"DELETED" => (message.deleted, idx + 1),
"FLAGGED" => (message.flagged, idx + 1),
"ANSWERED" => (message.answered, idx + 1),
"DRAFT" => (message.draft, idx + 1),
"LARGER" => {
let value = tokens.get(idx + 1).and_then(|s| s.parse::<usize>().ok());
let matched = value.is_some_and(|v| message.data.len() > v);
(matched, idx + 2)
}
"SMALLER" => {
let value = tokens.get(idx + 1).and_then(|s| s.parse::<usize>().ok());
let matched = value.is_some_and(|v| message.data.len() < v);
(matched, idx + 2)
}
"ON" => {
let value = tokens.get(idx + 1).map(String::as_str);
(date_on_match(message, value), idx + 2)
}
"SENTSINCE" => {
let value = tokens.get(idx + 1).map(String::as_str);
(sent_date_matches(message, value, true), idx + 2)
}
"SENTBEFORE" => {
let value = tokens.get(idx + 1).map(String::as_str);
(sent_date_matches(message, value, false), idx + 2)
}
"SUBJECT" => {
let value = tokens.get(idx + 1).map(String::as_str);
(header_matches(message, "Subject", value), idx + 2)
}
"FROM" => {
let value = tokens.get(idx + 1).map(String::as_str);
(header_matches(message, "From", value), idx + 2)
}
"TO" => {
let value = tokens.get(idx + 1).map(String::as_str);
(header_matches(message, "To", value), idx + 2)
}
"TEXT" => {
let value = tokens.get(idx + 1).map(String::as_str);
(text_matches(message, value), idx + 2)
}
"HEADER" => {
let name = tokens.get(idx + 1).map(String::as_str);
let value = tokens.get(idx + 2).map(String::as_str);
if let (Some(name), Some(value)) = (name, value) {
(header_matches(message, name, Some(value)), idx + 3)
} else {
(false, tokens.len())
}
}
"SINCE" => {
let value = tokens.get(idx + 1).map(String::as_str);
(date_matches(message, value, true), idx + 2)
}
"BEFORE" => {
let value = tokens.get(idx + 1).map(String::as_str);
(date_matches(message, value, false), idx + 2)
}
_ => (false, idx + 1),
}
}
fn header_matches(message: &Message, name: &str, value: Option<&str>) -> bool {
let Some(value) = value else {
return false;
};
let header = header_value(&message.data, name).unwrap_or_default();
header
.to_ascii_lowercase()
.contains(&value.to_ascii_lowercase())
}
fn text_matches(message: &Message, value: Option<&str>) -> bool {
let Some(value) = value else {
return false;
};
let haystack = String::from_utf8_lossy(&message.data).to_ascii_lowercase();
haystack.contains(&value.to_ascii_lowercase())
}
fn date_matches(message: &Message, value: Option<&str>, since: bool) -> bool {
let Some(value) = value else {
return false;
};
let target = parse_imap_date(value);
let message_date = parse_internal_date_only(&message.internal_date);
let (Some(target), Some(message_date)) = (target, message_date) else {
return false;
};
if since {
message_date >= target
} else {
message_date < target
}
}
fn date_on_match(message: &Message, value: Option<&str>) -> bool {
let Some(value) = value else {
return false;
};
let target = parse_imap_date(value);
let message_date = parse_internal_date_only(&message.internal_date);
let (Some(target), Some(message_date)) = (target, message_date) else {
return false;
};
message_date == target
}
fn sent_date_matches(message: &Message, value: Option<&str>, since: bool) -> bool {
let Some(value) = value else {
return false;
};
let target = parse_imap_date(value);
let date_header = header_value(&message.data, "Date").unwrap_or_default();
let message_date = parse_rfc2822_date_only(&date_header);
let (Some(target), Some(message_date)) = (target, message_date) else {
return false;
};
if since {
message_date >= target
} else {
message_date < target
}
}
fn parse_rfc2822_date_only(value: &str) -> Option<(i32, u32, u32)> {
let value = value.trim();
let value = if let Some((_, rest)) = value.split_once(',') {
rest.trim()
} else {
value
};
let mut parts = value.split_whitespace();
let day = parts.next()?.parse::<u32>().ok()?;
let mon = parts.next()?;
let year = parts.next()?.parse::<i32>().ok()?;
let month = match mon.to_ascii_lowercase().as_str() {
"jan" => 1,
"feb" => 2,
"mar" => 3,
"apr" => 4,
"may" => 5,
"jun" => 6,
"jul" => 7,
"aug" => 8,
"sep" => 9,
"oct" => 10,
"nov" => 11,
"dec" => 12,
_ => return None,
};
Some((year, month, day))
}
fn parse_internal_date_only(value: &str) -> Option<(i32, u32, u32)> {
let date_part = value.split_whitespace().next()?;
parse_imap_date(date_part)
}
fn parse_imap_date(value: &str) -> Option<(i32, u32, u32)> {
let (day, rest) = value.split_once('-')?;
let (mon, year) = rest.split_once('-')?;
let day = day.parse::<u32>().ok()?;
let year = year.parse::<i32>().ok()?;
let month = match mon.to_ascii_lowercase().as_str() {
"jan" => 1,
"feb" => 2,
"mar" => 3,
"apr" => 4,
"may" => 5,
"jun" => 6,
"jul" => 7,
"aug" => 8,
"sep" => 9,
"oct" => 10,
"nov" => 11,
"dec" => 12,
_ => return None,
};
Some((year, month, day))
}
async fn write_raw(writer: &mut ImapReader, data: &[u8]) -> io::Result<()> {
writer.get_mut().write_all(data).await
}