use std::{
io,
sync::{Arc, Mutex},
};
use super::mime;
use super::{ImapReader, parse_seq_range, parse_uid_range, write_raw};
use crate::store::{Message, Store};
#[derive(Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub(super) struct FetchRequest {
pub(super) include_rfc822: bool,
pub(super) include_body: bool,
pub(super) include_size: bool,
pub(super) include_flags: bool,
pub(super) include_internal_date: bool,
pub(super) header: Option<HeaderFetchKind>,
pub(super) include_envelope: bool,
pub(super) include_bodystructure: bool,
pub(super) body_peek: bool,
pub(super) body_sections: Vec<BodySection>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum HeaderFetchKind {
Rfc822Header,
BodyHeader,
}
#[derive(Debug, Clone)]
pub(super) struct BodySection {
pub(super) section: String,
pub(super) peek: bool,
pub(super) partial: Option<(usize, usize)>,
}
pub(super) fn parse_fetch_request(line: &str) -> FetchRequest {
let upper = line.to_ascii_uppercase();
let mut include_rfc822 = false;
let mut include_body = false;
let mut include_size = false;
let mut include_flags = false;
let mut include_internal_date = false;
let mut header = None;
let mut include_envelope = false;
let mut include_bodystructure = false;
let mut body_peek = false;
let mut body_sections = Vec::new();
let fetch_pos = upper.find("FETCH").unwrap_or(0);
let items_part = &line[fetch_pos..];
parse_body_sections(items_part, &mut body_sections);
for raw in upper
.split_whitespace()
.map(|token| token.trim_matches(|c| c == '(' || c == ')'))
{
match raw {
"RFC822" => include_rfc822 = true,
"RFC822.SIZE" => include_size = true,
"RFC822.HEADER" => header = Some(HeaderFetchKind::Rfc822Header),
"BODY[]" => include_body = true,
"BODY[HEADER]" => header = Some(HeaderFetchKind::BodyHeader),
"BODY.PEEK[]" => {
include_body = true;
body_peek = true;
}
"FLAGS" => include_flags = true,
"INTERNALDATE" => include_internal_date = true,
"ENVELOPE" => include_envelope = true,
"BODYSTRUCTURE" => include_bodystructure = true,
_ => {}
}
}
let has_any = include_rfc822
|| include_body
|| include_size
|| include_flags
|| include_internal_date
|| header.is_some()
|| include_envelope
|| include_bodystructure
|| !body_sections.is_empty();
FetchRequest {
include_rfc822: include_rfc822 || !has_any,
include_body,
include_size,
include_flags: include_flags || !has_any,
include_internal_date,
header,
include_envelope,
include_bodystructure,
body_peek,
body_sections,
}
}
fn parse_body_sections(line: &str, sections: &mut Vec<BodySection>) {
let upper = line.to_uppercase();
let mut idx = 0;
while idx < upper.len() {
let peek;
let start;
if let Some(pos) = upper[idx..].find("BODY.PEEK[") {
peek = true;
start = idx + pos + 10; } else if let Some(pos) = upper[idx..].find("BODY[") {
let candidate_pos = idx + pos;
if candidate_pos + 5 < upper.len()
&& upper[candidate_pos..].starts_with("BODYSTRUCTURE")
{
idx = candidate_pos + 13;
continue;
}
peek = false;
start = idx + pos + 5; } else {
break;
}
let Some(end_bracket) = upper[start..].find(']') else {
break;
};
let section = line[start..start + end_bracket].to_string();
idx = start + end_bracket + 1;
let partial = if idx < line.len() && line.as_bytes()[idx] == b'<' {
if let Some(close) = line[idx..].find('>') {
let partial_spec = &line[idx + 1..idx + close];
idx += close + 1;
parse_partial(partial_spec)
} else {
None
}
} else {
None
};
if !section.is_empty() {
sections.push(BodySection {
section,
peek,
partial,
});
}
}
}
fn parse_partial(spec: &str) -> Option<(usize, usize)> {
let mut parts = spec.split('.');
let offset: usize = parts.next()?.parse().ok()?;
let length: usize = parts.next()?.parse().ok()?;
Some((offset, length))
}
pub(super) async fn send_fetch_seq(
writer: &mut ImapReader,
store: &Arc<Mutex<Store>>,
user: &str,
mailbox: &str,
range: &str,
req: &FetchRequest,
) -> io::Result<()> {
let messages = store
.lock()
.expect("store lock poisoned")
.list(user, mailbox);
let max_seq = u32::try_from(messages.len()).unwrap_or(u32::MAX);
let (start, end) = parse_seq_range(range, max_seq);
for (idx, message) in messages.iter().enumerate() {
let Ok(seq) = u32::try_from(idx + 1) else {
continue;
};
if seq < start || seq > end {
continue;
}
let flags = format_flags(message);
write_fetch_response(writer, seq, message, &flags, req).await?;
}
Ok(())
}
pub(super) async fn send_fetch_uid(
writer: &mut ImapReader,
store: &Arc<Mutex<Store>>,
user: &str,
mailbox: &str,
range: &str,
req: &FetchRequest,
) -> io::Result<()> {
let messages = store
.lock()
.expect("store lock poisoned")
.list(user, mailbox);
let max_uid = u32::try_from(messages.len()).unwrap_or(u32::MAX);
let (start, end) = parse_uid_range(range, max_uid);
for (idx, message) in messages.iter().enumerate() {
if message.uid < start || message.uid > end {
continue;
}
let seq = idx + 1;
let flags = format_flags(message);
let Ok(seq) = u32::try_from(seq) else {
continue;
};
write_fetch_response(writer, seq, message, &flags, req).await?;
}
Ok(())
}
pub(super) async fn write_fetch_response(
writer: &mut ImapReader,
seq: u32,
message: &Message,
flags: &str,
req: &FetchRequest,
) -> io::Result<()> {
let mut items = Vec::new();
items.push(format!("UID {}", message.uid));
if req.include_flags {
items.push(format!("FLAGS ({flags})"));
}
if req.include_internal_date {
items.push(format!("INTERNALDATE \"{}\"", message.internal_date));
}
if req.include_size {
items.push(format!("RFC822.SIZE {}", message.data.len()));
}
if req.include_envelope {
items.push(build_envelope(message));
}
if req.include_bodystructure {
items.push(build_bodystructure(message));
}
for section in &req.body_sections {
if let Some(mut data) = mime::extract_section(&message.data, §ion.section) {
if let Some((offset, length)) = section.partial {
let start = offset.min(data.len());
let end = (offset + length).min(data.len());
data = &data[start..end];
}
let label = if section.peek {
format!("BODY.PEEK[{}]", section.section)
} else {
format!("BODY[{}]", section.section)
};
let label = if let Some((offset, _)) = section.partial {
format!("{}<{}>", label, offset)
} else {
label
};
items.push(format!("{} {{{}}}", label, data.len()));
}
}
let mut prefix = format!("* {} FETCH ({}", seq, items.join(" "));
for section in &req.body_sections {
if let Some(mut data) = mime::extract_section(&message.data, §ion.section) {
if let Some((offset, length)) = section.partial {
let start = offset.min(data.len());
let end = (offset + length).min(data.len());
data = &data[start..end];
}
prefix.push_str("\r\n");
write_raw(writer, prefix.as_bytes()).await?;
write_raw(writer, data).await?;
prefix = String::new();
}
}
if let Some(header_kind) = req.header {
let header_bytes = extract_headers(&message.data);
let label = match header_kind {
HeaderFetchKind::Rfc822Header => "RFC822.HEADER",
HeaderFetchKind::BodyHeader => "BODY[HEADER]",
};
let literal = format!(" {} {{{}}}\r\n", label, header_bytes.len());
prefix.push_str(&literal);
write_raw(writer, prefix.as_bytes()).await?;
write_raw(writer, header_bytes).await?;
write_raw(writer, b"\r\n").await?;
write_raw(writer, b")\r\n").await?;
return Ok(());
}
if req.include_rfc822 {
let literal = format!(" RFC822 {{{}}}\r\n", message.data.len());
prefix.push_str(&literal);
write_raw(writer, prefix.as_bytes()).await?;
write_raw(writer, &message.data).await?;
write_raw(writer, b"\r\n").await?;
write_raw(writer, b")\r\n").await?;
return Ok(());
}
if req.include_body {
let label = if req.body_peek {
"BODY.PEEK[]"
} else {
"BODY[]"
};
let literal = format!(" {} {{{}}}\r\n", label, message.data.len());
prefix.push_str(&literal);
write_raw(writer, prefix.as_bytes()).await?;
write_raw(writer, &message.data).await?;
write_raw(writer, b"\r\n").await?;
write_raw(writer, b")\r\n").await?;
return Ok(());
}
prefix.push_str(")\r\n");
write_raw(writer, prefix.as_bytes()).await?;
Ok(())
}
pub(super) fn format_flags(message: &Message) -> String {
let mut flags = Vec::new();
if message.seen {
flags.push("\\Seen");
}
if message.flagged {
flags.push("\\Flagged");
}
if message.deleted {
flags.push("\\Deleted");
}
if message.answered {
flags.push("\\Answered");
}
if message.draft {
flags.push("\\Draft");
}
flags.join(" ")
}
fn extract_headers(data: &[u8]) -> &[u8] {
let needle = b"\r\n\r\n";
if let Some(pos) = data.windows(needle.len()).position(|w| w == needle) {
&data[..pos + needle.len()]
} else {
data
}
}
#[allow(dead_code)]
fn extract_body(data: &[u8]) -> &[u8] {
let needle = b"\r\n\r\n";
if let Some(pos) = data.windows(needle.len()).position(|w| w == needle) {
&data[pos + needle.len()..]
} else {
&[]
}
}
pub(crate) fn header_value(data: &[u8], name: &str) -> Option<String> {
let needle = name.to_ascii_lowercase();
for line in data.split(|b| *b == b'\n') {
let line = line.strip_suffix(b"\r").unwrap_or(line);
if line.is_empty() {
break;
}
let line_str = String::from_utf8_lossy(line);
let lower = line_str.to_ascii_lowercase();
if lower.starts_with(&format!("{needle}:")) {
let value = line_str[needle.len() + 1..].trim();
if value.is_empty() {
return None;
}
return Some(value.to_string());
}
}
None
}
fn build_envelope(message: &Message) -> String {
let date = header_value(&message.data, "Date")
.map_or_else(|| "NIL".to_string(), |v| format!("\"{v}\""));
let subject = header_value(&message.data, "Subject")
.map_or_else(|| "NIL".to_string(), |v| format!("\"{v}\""));
format!("ENVELOPE ({date} {subject} NIL NIL NIL NIL NIL NIL NIL NIL)")
}
fn build_bodystructure(message: &Message) -> String {
let part = mime::parse_mime(&message.data);
format!("BODYSTRUCTURE {}", mime::format_bodystructure(&part))
}