use crate::command::{StoreMode, UidSubcommand};
use crate::handler::HandlerContext;
use crate::mailbox_registry::MailboxEvent;
use crate::response::ImapResponse;
use crate::session::{ImapSession, ImapState};
use rusmes_proto::MessageId;
use rusmes_storage::{MessageFlags, MessageMetadata, SearchCriteria};
pub(crate) async fn handle_fetch(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
sequence: &str,
items: &[String],
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let all_messages = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let sequence_numbers = parse_sequence_numbers(sequence, all_messages.len())?;
let mut responses = Vec::new();
for seq_num in sequence_numbers {
if seq_num > 0 && seq_num <= all_messages.len() {
let metadata = &all_messages[seq_num - 1];
if let Some(mail) = ctx.message_store.get_message(metadata.message_id()).await? {
let fetch_items = build_fetch_items(&mail, metadata, items).await;
responses.push(format!("* {} FETCH ({})", seq_num, fetch_items));
}
}
}
let mut full_response = responses.join("\r\n");
if !full_response.is_empty() {
full_response.push_str("\r\n");
}
full_response.push_str(&format!("{} OK FETCH completed", tag));
Ok(ImapResponse::new(None, "", full_response))
}
pub(crate) async fn handle_store(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
sequence: &str,
mode: StoreMode,
flags: &[String],
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => *mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let message_ids = parse_sequence_set(sequence)?;
let msg_flags = build_message_flags(flags);
if !message_ids.is_empty() {
match mode {
StoreMode::Replace => {
ctx.message_store.set_flags(&message_ids, msg_flags).await?;
}
StoreMode::Add => {
ctx.message_store.set_flags(&message_ids, msg_flags).await?;
}
StoreMode::Remove => {
ctx.message_store.set_flags(&message_ids, msg_flags).await?;
}
}
let all_metadata = ctx.message_store.get_mailbox_messages(&mailbox_id).await?;
let flag_strings: Vec<String> = flags.to_vec();
let target_uids: Vec<u32> = all_metadata
.iter()
.filter(|m| message_ids.contains(m.message_id()))
.map(|m| m.uid())
.collect();
for uid in target_uids {
ctx.mailbox_registry.publish(
mailbox_id,
MailboxEvent::FlagsChanged {
uid,
flags: flag_strings.clone(),
},
);
}
}
Ok(ImapResponse::ok(tag, "STORE completed"))
}
pub(crate) async fn handle_search(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
criteria: &[String],
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let search_criteria = parse_search_criteria(criteria);
let message_ids = ctx
.message_store
.search(mailbox_id, search_criteria)
.await?;
let ids_str: Vec<String> = message_ids.iter().map(|id| id.to_string()).collect();
let response = format!(
"* SEARCH {}\r\n{} OK SEARCH completed",
ids_str.join(" "),
tag
);
Ok(ImapResponse::new(None, "", response))
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_append(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
mailbox: &str,
flags: &[String],
_date_time: Option<&str>,
message_literal: &[u8],
) -> anyhow::Result<ImapResponse> {
if !matches!(
session.state(),
ImapState::Authenticated | ImapState::Selected { .. }
) {
return Ok(ImapResponse::no(tag, "Not authenticated"));
}
let username = match &session.username {
Some(u) => u.clone(),
None => return Ok(ImapResponse::no(tag, "No username in session")),
};
let mailboxes = ctx.mailbox_store.list_mailboxes(&username).await?;
let mailbox_obj = mailboxes.iter().find(|m| m.path().name() == Some(mailbox));
let mailbox_id = match mailbox_obj {
Some(mb) => *mb.id(),
None => return Ok(ImapResponse::no(tag, "[TRYCREATE] Mailbox does not exist")),
};
let message_data = bytes::Bytes::from(message_literal.to_vec());
let (headers, body) = parse_message_data(&message_data)?;
use rusmes_proto::{MessageBody, MimeMessage};
let mime_message = MimeMessage::new(headers, MessageBody::Small(body));
let sender = extract_sender_from_headers(mime_message.headers());
let recipients = extract_recipients_from_headers(mime_message.headers());
use rusmes_proto::Mail;
let mut mail = Mail::new(sender, recipients, mime_message, None, None);
use rusmes_proto::MailState;
mail.state = MailState::LocalDelivery;
let metadata = ctx.message_store.append_message(&mailbox_id, mail).await?;
if !flags.is_empty() {
let msg_flags = build_message_flags(flags);
ctx.message_store
.set_flags(&[*metadata.message_id()], msg_flags)
.await?;
}
let new_count = ctx
.metadata_store
.get_mailbox_counters(&mailbox_id)
.await
.map(|c| c.exists)
.unwrap_or(0);
ctx.mailbox_registry
.publish(mailbox_id, MailboxEvent::Exists { count: new_count });
let uid_validity = mailbox_obj.map(|mb| mb.uid_validity()).unwrap_or(0);
Ok(ImapResponse::ok(
tag,
format!(
"[APPENDUID {} {}] APPEND completed",
uid_validity,
metadata.uid()
),
))
}
pub(crate) async fn handle_copy(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
sequence: &str,
dest_mailbox: &str,
) -> anyhow::Result<ImapResponse> {
let _source_mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let username = match &session.username {
Some(u) => u.clone(),
None => return Ok(ImapResponse::no(tag, "No username in session")),
};
let mailboxes = ctx.mailbox_store.list_mailboxes(&username).await?;
let dest_mailbox_obj = mailboxes
.iter()
.find(|m| m.path().name() == Some(dest_mailbox));
let dest_mailbox_id = match dest_mailbox_obj {
Some(mb) => mb.id(),
None => {
return Ok(ImapResponse::no(
tag,
"[TRYCREATE] Destination mailbox does not exist",
))
}
};
let message_ids = parse_sequence_set(sequence)?;
if message_ids.is_empty() {
return Ok(ImapResponse::ok(tag, "COPY completed (no messages)"));
}
let copied_metadata = ctx
.message_store
.copy_messages(&message_ids, dest_mailbox_id)
.await?;
if !copied_metadata.is_empty() {
let source_uids: Vec<String> = message_ids.iter().map(|id| id.to_string()).collect();
let dest_uids: Vec<String> = copied_metadata
.iter()
.map(|m| m.uid().to_string())
.collect();
let uid_validity = dest_mailbox_obj.map(|mb| mb.uid_validity()).unwrap_or(0);
Ok(ImapResponse::ok(
tag,
format!(
"[COPYUID {} {} {}] COPY completed",
uid_validity,
source_uids.join(","),
dest_uids.join(",")
),
))
} else {
Ok(ImapResponse::ok(tag, "COPY completed"))
}
}
pub(crate) async fn handle_move(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
sequence: &str,
dest_mailbox: &str,
) -> anyhow::Result<ImapResponse> {
let _source_mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let username = match &session.username {
Some(u) => u.clone(),
None => return Ok(ImapResponse::no(tag, "No username in session")),
};
let mailboxes = ctx.mailbox_store.list_mailboxes(&username).await?;
let dest_mailbox_obj = mailboxes
.iter()
.find(|m| m.path().name() == Some(dest_mailbox));
let dest_mailbox_id = match dest_mailbox_obj {
Some(mb) => mb.id(),
None => {
return Ok(ImapResponse::no(
tag,
"[TRYCREATE] Destination mailbox does not exist",
))
}
};
let message_ids = parse_sequence_set(sequence)?;
if message_ids.is_empty() {
return Ok(ImapResponse::ok(tag, "MOVE completed (no messages)"));
}
let copied_metadata = ctx
.message_store
.copy_messages(&message_ids, dest_mailbox_id)
.await?;
let mut delete_flags = MessageFlags::new();
delete_flags.set_deleted(true);
ctx.message_store
.set_flags(&message_ids, delete_flags)
.await?;
ctx.message_store.delete_messages(&message_ids).await?;
if !copied_metadata.is_empty() {
let source_uids: Vec<String> = message_ids.iter().map(|id| id.to_string()).collect();
let dest_uids: Vec<String> = copied_metadata
.iter()
.map(|m| m.uid().to_string())
.collect();
let uid_validity = dest_mailbox_obj.map(|mb| mb.uid_validity()).unwrap_or(0);
Ok(ImapResponse::ok(
tag,
format!(
"[COPYUID {} {} {}] MOVE completed",
uid_validity,
source_uids.join(","),
dest_uids.join(",")
),
))
} else {
Ok(ImapResponse::ok(tag, "MOVE completed"))
}
}
pub(crate) async fn handle_expunge(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let messages = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let mut deleted_messages = Vec::new();
let mut expunge_responses = Vec::new();
let mut expunge_seqs: Vec<u32> = Vec::new();
for (seq_num, metadata) in messages.iter().enumerate() {
if metadata.flags().is_deleted() {
deleted_messages.push(*metadata.message_id());
let seq = (seq_num + 1) as u32;
expunge_responses.push(format!("* {} EXPUNGE", seq));
expunge_seqs.push(seq);
}
}
if !deleted_messages.is_empty() {
ctx.message_store.delete_messages(&deleted_messages).await?;
for seq in &expunge_seqs {
ctx.mailbox_registry
.publish(*mailbox_id, MailboxEvent::Expunge { seq: *seq });
}
}
let mut full_response = expunge_responses.join("\r\n");
if !full_response.is_empty() {
full_response.push_str("\r\n");
}
full_response.push_str(&format!("{} OK EXPUNGE completed", tag));
Ok(ImapResponse::new(None, "", full_response))
}
pub(crate) async fn handle_close(
ctx: &HandlerContext,
session: &mut ImapSession,
tag: &str,
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let messages = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let deleted_messages: Vec<MessageId> = messages
.iter()
.filter(|m| m.flags().is_deleted())
.map(|m| *m.message_id())
.collect();
if !deleted_messages.is_empty() {
ctx.message_store.delete_messages(&deleted_messages).await?;
}
session.state = ImapState::Authenticated;
session.mailbox_event_rx = None;
Ok(ImapResponse::ok(tag, "CLOSE completed"))
}
pub(crate) async fn handle_uid(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
subcommand: &UidSubcommand,
) -> anyhow::Result<ImapResponse> {
match session.state() {
ImapState::Selected { .. } => {}
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
}
match subcommand {
UidSubcommand::Fetch { sequence, items } => {
handle_uid_fetch(ctx, session, tag, sequence, items).await
}
UidSubcommand::Store {
sequence,
mode,
flags,
} => handle_uid_store(ctx, session, tag, sequence, mode.clone(), flags).await,
UidSubcommand::Search { criteria } => handle_uid_search(ctx, session, tag, criteria).await,
UidSubcommand::Copy { sequence, mailbox } => {
handle_uid_copy(ctx, session, tag, sequence, mailbox).await
}
UidSubcommand::Move { sequence, mailbox } => {
handle_uid_move(ctx, session, tag, sequence, mailbox).await
}
UidSubcommand::Expunge { sequence } => {
handle_uid_expunge(ctx, session, tag, sequence).await
}
}
}
async fn handle_uid_fetch(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
uid_sequence: &str,
items: &[String],
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let all_metadata = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let uid_set = parse_uid_sequence_set(uid_sequence, &all_metadata)?;
let matching_metadata: Vec<_> = all_metadata
.iter()
.filter(|m| uid_set.contains(&m.uid()))
.collect();
let mut responses = Vec::new();
for (seq_num, metadata) in matching_metadata.iter().enumerate() {
if let Some(mail) = ctx.message_store.get_message(metadata.message_id()).await? {
let fetch_items = build_fetch_items(&mail, metadata, items).await;
responses.push(format!("* {} FETCH ({})", seq_num + 1, fetch_items));
}
}
let mut full_response = responses.join("\r\n");
if !full_response.is_empty() {
full_response.push_str("\r\n");
}
full_response.push_str(&format!("{} OK UID FETCH completed", tag));
Ok(ImapResponse::new(None, "", full_response))
}
async fn handle_uid_store(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
uid_sequence: &str,
mode: StoreMode,
flags: &[String],
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let all_metadata = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let uid_set = parse_uid_sequence_set(uid_sequence, &all_metadata)?;
let message_ids: Vec<MessageId> = all_metadata
.iter()
.filter(|m| uid_set.contains(&m.uid()))
.map(|m| *m.message_id())
.collect();
let target_uids: Vec<u32> = all_metadata
.iter()
.filter(|m| uid_set.contains(&m.uid()))
.map(|m| m.uid())
.collect();
if message_ids.is_empty() {
return Ok(ImapResponse::ok(tag, "UID STORE completed (no messages)"));
}
let msg_flags = build_message_flags(flags);
match mode {
StoreMode::Replace => {
ctx.message_store.set_flags(&message_ids, msg_flags).await?;
}
StoreMode::Add => {
ctx.message_store.set_flags(&message_ids, msg_flags).await?;
}
StoreMode::Remove => {
ctx.message_store.set_flags(&message_ids, msg_flags).await?;
}
}
let flag_strings: Vec<String> = flags.to_vec();
for uid in target_uids {
ctx.mailbox_registry.publish(
*mailbox_id,
MailboxEvent::FlagsChanged {
uid,
flags: flag_strings.clone(),
},
);
}
Ok(ImapResponse::ok(tag, "UID STORE completed"))
}
async fn handle_uid_search(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
criteria: &[String],
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let search_criteria = parse_search_criteria(criteria);
let message_ids = ctx
.message_store
.search(mailbox_id, search_criteria)
.await?;
let all_metadata = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let uids: Vec<String> = all_metadata
.iter()
.filter(|m| message_ids.contains(m.message_id()))
.map(|m| m.uid().to_string())
.collect();
let response = format!(
"* SEARCH {}\r\n{} OK UID SEARCH completed",
uids.join(" "),
tag
);
Ok(ImapResponse::new(None, "", response))
}
async fn handle_uid_copy(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
uid_sequence: &str,
dest_mailbox: &str,
) -> anyhow::Result<ImapResponse> {
let source_mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let username = match &session.username {
Some(u) => u.clone(),
None => return Ok(ImapResponse::no(tag, "No username in session")),
};
let mailboxes = ctx.mailbox_store.list_mailboxes(&username).await?;
let dest_mailbox_obj = mailboxes
.iter()
.find(|m| m.path().name() == Some(dest_mailbox));
let dest_mailbox_id = match dest_mailbox_obj {
Some(mb) => mb.id(),
None => {
return Ok(ImapResponse::no(
tag,
"[TRYCREATE] Destination mailbox does not exist",
))
}
};
let all_metadata = ctx
.message_store
.get_mailbox_messages(source_mailbox_id)
.await?;
let uid_set = parse_uid_sequence_set(uid_sequence, &all_metadata)?;
let message_ids: Vec<MessageId> = all_metadata
.iter()
.filter(|m| uid_set.contains(&m.uid()))
.map(|m| *m.message_id())
.collect();
if message_ids.is_empty() {
return Ok(ImapResponse::ok(tag, "UID COPY completed (no messages)"));
}
let copied_metadata = ctx
.message_store
.copy_messages(&message_ids, dest_mailbox_id)
.await?;
if !copied_metadata.is_empty() {
let source_uids: Vec<String> = all_metadata
.iter()
.filter(|m| message_ids.contains(m.message_id()))
.map(|m| m.uid().to_string())
.collect();
let dest_uids: Vec<String> = copied_metadata
.iter()
.map(|m| m.uid().to_string())
.collect();
let uid_validity = dest_mailbox_obj.map(|mb| mb.uid_validity()).unwrap_or(0);
Ok(ImapResponse::ok(
tag,
format!(
"[COPYUID {} {} {}] UID COPY completed",
uid_validity,
source_uids.join(","),
dest_uids.join(",")
),
))
} else {
Ok(ImapResponse::ok(tag, "UID COPY completed"))
}
}
async fn handle_uid_move(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
uid_sequence: &str,
dest_mailbox: &str,
) -> anyhow::Result<ImapResponse> {
let source_mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let username = match &session.username {
Some(u) => u.clone(),
None => return Ok(ImapResponse::no(tag, "No username in session")),
};
let mailboxes = ctx.mailbox_store.list_mailboxes(&username).await?;
let dest_mailbox_obj = mailboxes
.iter()
.find(|m| m.path().name() == Some(dest_mailbox));
let dest_mailbox_id = match dest_mailbox_obj {
Some(mb) => mb.id(),
None => {
return Ok(ImapResponse::no(
tag,
"[TRYCREATE] Destination mailbox does not exist",
))
}
};
let all_metadata = ctx
.message_store
.get_mailbox_messages(source_mailbox_id)
.await?;
let uid_set = parse_uid_sequence_set(uid_sequence, &all_metadata)?;
let message_ids: Vec<MessageId> = all_metadata
.iter()
.filter(|m| uid_set.contains(&m.uid()))
.map(|m| *m.message_id())
.collect();
if message_ids.is_empty() {
return Ok(ImapResponse::ok(tag, "UID MOVE completed (no messages)"));
}
let copied_metadata = ctx
.message_store
.copy_messages(&message_ids, dest_mailbox_id)
.await?;
let mut delete_flags = MessageFlags::new();
delete_flags.set_deleted(true);
ctx.message_store
.set_flags(&message_ids, delete_flags)
.await?;
ctx.message_store.delete_messages(&message_ids).await?;
if !copied_metadata.is_empty() {
let source_uids: Vec<String> = all_metadata
.iter()
.filter(|m| message_ids.contains(m.message_id()))
.map(|m| m.uid().to_string())
.collect();
let dest_uids: Vec<String> = copied_metadata
.iter()
.map(|m| m.uid().to_string())
.collect();
let uid_validity = dest_mailbox_obj.map(|mb| mb.uid_validity()).unwrap_or(0);
Ok(ImapResponse::ok(
tag,
format!(
"[COPYUID {} {} {}] UID MOVE completed",
uid_validity,
source_uids.join(","),
dest_uids.join(",")
),
))
} else {
Ok(ImapResponse::ok(tag, "UID MOVE completed"))
}
}
async fn handle_uid_expunge(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
uid_sequence: &str,
) -> anyhow::Result<ImapResponse> {
let mailbox_id = match session.state() {
ImapState::Selected { mailbox_id } => mailbox_id,
_ => return Ok(ImapResponse::no(tag, "No mailbox selected")),
};
let all_metadata = ctx.message_store.get_mailbox_messages(mailbox_id).await?;
let uid_set = parse_uid_sequence_set(uid_sequence, &all_metadata)?;
let mut deleted_messages = Vec::new();
let mut expunge_responses = Vec::new();
let mut expunge_seqs: Vec<u32> = Vec::new();
for (seq_num, metadata) in all_metadata.iter().enumerate() {
if uid_set.contains(&metadata.uid()) && metadata.flags().is_deleted() {
deleted_messages.push(*metadata.message_id());
let seq = (seq_num + 1) as u32;
expunge_responses.push(format!("* {} EXPUNGE", seq));
expunge_seqs.push(seq);
}
}
if !deleted_messages.is_empty() {
ctx.message_store.delete_messages(&deleted_messages).await?;
for seq in &expunge_seqs {
ctx.mailbox_registry
.publish(*mailbox_id, MailboxEvent::Expunge { seq: *seq });
}
}
let mut full_response = expunge_responses.join("\r\n");
if !full_response.is_empty() {
full_response.push_str("\r\n");
}
full_response.push_str(&format!("{} OK UID EXPUNGE completed", tag));
Ok(ImapResponse::new(None, "", full_response))
}
pub(crate) fn parse_sequence_set(sequence: &str) -> anyhow::Result<Vec<MessageId>> {
let _ = sequence;
Ok(Vec::new())
}
pub(crate) fn parse_sequence_numbers(sequence: &str, max: usize) -> anyhow::Result<Vec<usize>> {
let mut numbers = Vec::new();
for part in sequence.split(',') {
let part = part.trim();
if part.contains(':') {
let range_parts: Vec<&str> = part.split(':').collect();
if range_parts.len() == 2 {
let start = range_parts[0].parse::<usize>().unwrap_or(1);
let end = if range_parts[1] == "*" {
max
} else {
range_parts[1].parse::<usize>().unwrap_or(max)
};
for n in start..=end.min(max) {
if !numbers.contains(&n) {
numbers.push(n);
}
}
}
} else if part == "*" {
if max > 0 && !numbers.contains(&max) {
numbers.push(max);
}
} else {
if let Ok(n) = part.parse::<usize>() {
if n > 0 && n <= max && !numbers.contains(&n) {
numbers.push(n);
}
}
}
}
numbers.sort();
Ok(numbers)
}
pub(crate) async fn build_fetch_items(
mail: &rusmes_proto::Mail,
metadata: &MessageMetadata,
items: &[String],
) -> String {
let mut fetch_items = Vec::new();
for item in items {
match item.to_uppercase().as_str() {
"FLAGS" => {
let flags = metadata.flags();
let mut flag_list = Vec::new();
if flags.is_seen() {
flag_list.push("\\Seen");
}
if flags.is_answered() {
flag_list.push("\\Answered");
}
if flags.is_flagged() {
flag_list.push("\\Flagged");
}
if flags.is_deleted() {
flag_list.push("\\Deleted");
}
if flags.is_draft() {
flag_list.push("\\Draft");
}
fetch_items.push(format!("FLAGS ({})", flag_list.join(" ")));
}
"UID" => {
fetch_items.push(format!("UID {}", metadata.uid()));
}
"BODY[]" | "BODY.PEEK[]" => {
let message = mail.message();
if let Ok(body_text) = message.extract_text().await {
let body_len = body_text.len();
fetch_items.push(format!("BODY[] {{{}}}\r\n{}", body_len, body_text));
} else {
fetch_items.push("BODY[] {0}\r\n".to_string());
}
}
"RFC822.SIZE" => {
fetch_items.push(format!("RFC822.SIZE {}", metadata.size()));
}
_ => {
}
}
}
fetch_items.join(" ")
}
pub(crate) fn parse_search_criteria(criteria: &[String]) -> SearchCriteria {
if criteria.is_empty() {
return SearchCriteria::All;
}
match criteria[0].to_uppercase().as_str() {
"ALL" => SearchCriteria::All,
"UNSEEN" => SearchCriteria::Unseen,
"SEEN" => SearchCriteria::Seen,
"FLAGGED" => SearchCriteria::Flagged,
"UNFLAGGED" => SearchCriteria::Unflagged,
"DELETED" => SearchCriteria::Deleted,
"UNDELETED" => SearchCriteria::Undeleted,
_ => SearchCriteria::All, }
}
fn build_message_flags(flags: &[String]) -> MessageFlags {
let mut msg_flags = MessageFlags::new();
for flag in flags {
match flag.to_uppercase().as_str() {
"\\SEEN" => msg_flags.set_seen(true),
"\\ANSWERED" => msg_flags.set_answered(true),
"\\FLAGGED" => msg_flags.set_flagged(true),
"\\DELETED" => msg_flags.set_deleted(true),
"\\DRAFT" => msg_flags.set_draft(true),
custom => msg_flags.add_custom(custom.to_string()),
}
}
msg_flags
}
pub(crate) fn parse_message_data(
data: &bytes::Bytes,
) -> anyhow::Result<(rusmes_proto::HeaderMap, bytes::Bytes)> {
use rusmes_proto::HeaderMap;
let data_str = String::from_utf8_lossy(data);
let mut headers = HeaderMap::new();
let mut body_start = 0;
let lines: Vec<&str> = data_str.split("\r\n").collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if line.is_empty() {
body_start = data_str[..data_str.len()]
.find("\r\n\r\n")
.map(|pos| pos + 4)
.unwrap_or(data.len());
break;
}
if let Some(colon_pos) = line.find(':') {
let name = line[..colon_pos].trim();
let value = line[colon_pos + 1..].trim();
headers.insert(name.to_string(), value.to_string());
}
i += 1;
}
let body = if body_start < data.len() {
data.slice(body_start..)
} else {
bytes::Bytes::new()
};
Ok((headers, body))
}
pub(crate) fn extract_sender_from_headers(
headers: &rusmes_proto::HeaderMap,
) -> Option<rusmes_proto::MailAddress> {
if let Some(from) = headers.get_first("from") {
let email = extract_email_address(from)?;
parse_email_address(&email)
} else {
None
}
}
pub(crate) fn extract_recipients_from_headers(
headers: &rusmes_proto::HeaderMap,
) -> Vec<rusmes_proto::MailAddress> {
let mut recipients = Vec::new();
if let Some(to_values) = headers.get("to") {
for to in to_values {
if let Some(email) = extract_email_address(to) {
if let Some(addr) = parse_email_address(&email) {
recipients.push(addr);
}
}
}
}
if let Some(cc_values) = headers.get("cc") {
for cc in cc_values {
if let Some(email) = extract_email_address(cc) {
if let Some(addr) = parse_email_address(&email) {
recipients.push(addr);
}
}
}
}
recipients
}
fn extract_email_address(s: &str) -> Option<String> {
if let Some(start) = s.find('<') {
if let Some(end) = s.find('>') {
return Some(s[start + 1..end].trim().to_string());
}
}
let trimmed = s.trim();
if trimmed.contains('@') {
return Some(trimmed.to_string());
}
None
}
fn parse_email_address(email: &str) -> Option<rusmes_proto::MailAddress> {
use rusmes_proto::{Domain, MailAddress};
if let Some(at_pos) = email.find('@') {
let local_part = &email[..at_pos];
let domain_str = &email[at_pos + 1..];
if let Ok(domain) = Domain::new(domain_str.to_string()) {
if let Ok(addr) = MailAddress::new(local_part, domain) {
return Some(addr);
}
}
}
None
}
fn parse_uid_sequence_set(
sequence: &str,
all_metadata: &[MessageMetadata],
) -> anyhow::Result<std::collections::HashSet<u32>> {
use std::collections::HashSet;
let mut uid_set = HashSet::new();
let max_uid = all_metadata.iter().map(|m| m.uid()).max().unwrap_or(0);
for part in sequence.split(',') {
let part = part.trim();
if part.contains(':') {
let range_parts: Vec<&str> = part.split(':').collect();
if range_parts.len() != 2 {
return Err(anyhow::anyhow!("Invalid UID range: {}", part));
}
let start = if range_parts[0] == "*" {
max_uid
} else {
range_parts[0].parse::<u32>()?
};
let end = if range_parts[1] == "*" {
max_uid
} else {
range_parts[1].parse::<u32>()?
};
for uid in start..=end {
uid_set.insert(uid);
}
} else {
let uid = if part == "*" {
max_uid
} else {
part.parse::<u32>()?
};
uid_set.insert(uid);
}
}
Ok(uid_set)
}