use crate::handler::HandlerContext;
use crate::response::ImapResponse;
use crate::session::{ImapSession, ImapState};
use rusmes_storage::MailboxPath;
pub(crate) async fn handle_select(
ctx: &HandlerContext,
session: &mut ImapSession,
tag: &str,
mailbox: &str,
read_only: bool,
) -> 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 mailbox_obj = if mailbox.eq_ignore_ascii_case("INBOX") {
if let Some(inbox_id) = ctx.mailbox_store.get_user_inbox(&username).await? {
ctx.mailbox_store.get_mailbox(&inbox_id).await?
} else {
None
}
} else {
let mailboxes = ctx.mailbox_store.list_mailboxes(&username).await?;
mailboxes
.iter()
.find(|m| m.path().name() == Some(mailbox))
.cloned()
};
match mailbox_obj {
Some(mb) => {
let mailbox_id = *mb.id();
let counters = ctx.metadata_store.get_mailbox_counters(&mailbox_id).await?;
session.state = ImapState::Selected { mailbox_id };
session.mailbox_event_rx = Some(ctx.mailbox_registry.subscribe(mailbox_id));
let mode = if read_only { "READ-ONLY" } else { "READ-WRITE" };
let response_text = format!(
"* {} EXISTS\r\n* {} RECENT\r\n* OK [UIDVALIDITY {}]\r\n* OK [UIDNEXT {}]\r\n* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n{} OK [{}] {} completed",
counters.exists,
counters.recent,
mb.uid_validity(),
mb.uid_next(),
tag,
mode,
if read_only { "EXAMINE" } else { "SELECT" }
);
Ok(ImapResponse::new(None, "", response_text))
}
None => Ok(ImapResponse::no(tag, "Mailbox does not exist")),
}
}
pub(crate) async fn handle_list(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
_reference: &str,
pattern: &str,
) -> 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 mut responses = Vec::new();
for mailbox in mailboxes {
if pattern == "*" || mailbox.path().name() == Some(pattern) {
let name = mailbox.path().name().unwrap_or("INBOX");
responses.push(format!(r#"* LIST () "/" "{}""#, name));
}
}
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 LIST completed", tag));
Ok(ImapResponse::new(None, "", full_response))
}
pub(crate) async fn handle_lsub(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
_reference: &str,
pattern: &str,
) -> 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 subscriptions = ctx.mailbox_store.list_subscriptions(&username).await?;
let mut responses = Vec::new();
for mailbox_name in subscriptions {
let matches = if pattern == "*" {
true
} else if pattern.contains('*') || pattern.contains('%') {
match_mailbox_pattern(&mailbox_name, pattern)
} else {
mailbox_name == pattern
};
if matches {
responses.push(format!(r#"* LSUB () "/" "{}""#, mailbox_name));
}
}
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 LSUB completed", tag));
Ok(ImapResponse::new(None, "", full_response))
}
pub(crate) async fn handle_subscribe(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
mailbox: &str,
) -> 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 mailbox_name = mailbox.trim_matches('"');
ctx.mailbox_store
.subscribe_mailbox(&username, mailbox_name.to_string())
.await?;
Ok(ImapResponse::ok(tag, "SUBSCRIBE completed"))
}
pub(crate) async fn handle_unsubscribe(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
mailbox: &str,
) -> 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 mailbox_name = mailbox.trim_matches('"');
ctx.mailbox_store
.unsubscribe_mailbox(&username, mailbox_name)
.await?;
Ok(ImapResponse::ok(tag, "UNSUBSCRIBE completed"))
}
pub(crate) async fn handle_create(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
mailbox: &str,
) -> 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 path = MailboxPath::new(username, vec![mailbox.to_string()]);
ctx.mailbox_store.create_mailbox(&path).await?;
Ok(ImapResponse::ok(tag, "CREATE completed"))
}
pub(crate) async fn handle_create_special_use(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
mailbox: &str,
special_use: &str,
) -> anyhow::Result<ImapResponse> {
use rusmes_storage::SpecialUseAttributes;
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 path = MailboxPath::new(username, vec![mailbox.to_string()]);
let attrs = SpecialUseAttributes::single(special_use.to_string());
ctx.mailbox_store
.create_mailbox_with_special_use(&path, attrs)
.await?;
Ok(ImapResponse::ok(tag, "CREATE completed"))
}
pub(crate) async fn handle_delete(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
mailbox: &str,
) -> 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));
match mailbox_obj {
Some(mb) => {
ctx.mailbox_store.delete_mailbox(mb.id()).await?;
Ok(ImapResponse::ok(tag, "DELETE completed"))
}
None => Ok(ImapResponse::no(tag, "Mailbox does not exist")),
}
}
pub(crate) async fn handle_rename(
ctx: &HandlerContext,
session: &ImapSession,
tag: &str,
old_mailbox: &str,
new_mailbox: &str,
) -> 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(old_mailbox));
match mailbox_obj {
Some(mb) => {
let new_path = MailboxPath::new(username, vec![new_mailbox.to_string()]);
ctx.mailbox_store.rename_mailbox(mb.id(), &new_path).await?;
Ok(ImapResponse::ok(tag, "RENAME completed"))
}
None => Ok(ImapResponse::no(tag, "Mailbox does not exist")),
}
}
pub(crate) async fn handle_idle(
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 counters = ctx.metadata_store.get_mailbox_counters(&mailbox_id).await?;
session.update_snapshot(counters.exists, counters.recent);
session.state = ImapState::Idle { mailbox_id };
session.tag = Some(tag.to_string());
Ok(ImapResponse::new(None, "+", "idling"))
}
pub(crate) async fn handle_namespace(
tag: &str,
session: &ImapSession,
) -> anyhow::Result<ImapResponse> {
match session.state() {
ImapState::NotAuthenticated => {
return Ok(ImapResponse::no(tag, "NAMESPACE requires authentication"));
}
ImapState::Logout => {
return Ok(ImapResponse::no(tag, "Already logged out"));
}
_ => {}
}
let personal = vec![("".to_string(), ".".to_string())];
let other_users: Vec<(String, String)> = Vec::new();
let shared: Vec<(String, String)> = Vec::new();
let personal_str = format_namespace_list(&personal);
let other_users_str = format_namespace_list(&other_users);
let shared_str = format_namespace_list(&shared);
let untagged_response = format!(
"* NAMESPACE {} {} {}",
personal_str, other_users_str, shared_str
);
let full_response = format!("{}\r\n{} OK NAMESPACE completed", untagged_response, tag);
Ok(ImapResponse::new(None, "", full_response))
}
fn format_namespace_list(namespaces: &[(String, String)]) -> String {
if namespaces.is_empty() {
"NIL".to_string()
} else {
let items: Vec<String> = namespaces
.iter()
.map(|(prefix, delim)| format!("(\"{}\" \"{}\")", prefix, delim))
.collect();
format!("({})", items.join(" "))
}
}
pub(crate) fn match_mailbox_pattern(name: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
let mut pattern_chars = pattern.chars().peekable();
let mut name_chars = name.chars().peekable();
loop {
match (pattern_chars.peek(), name_chars.peek()) {
(None, None) => return true,
(None, Some(_)) => return false,
(Some(&'*'), _) => {
pattern_chars.next();
if pattern_chars.peek().is_none() {
return true;
}
let rest_pattern: String = pattern_chars.collect();
for i in 0..=name_chars.clone().count() {
let rest_name: String = name_chars.clone().skip(i).collect();
if match_mailbox_pattern(&rest_name, &rest_pattern) {
return true;
}
}
return false;
}
(Some(&'%'), _) => {
pattern_chars.next();
if pattern_chars.peek().is_none() {
return !name_chars.clone().any(|c| c == '/');
}
let rest_pattern: String = pattern_chars.collect();
for i in 0..=name_chars.clone().count() {
let rest_name: String = name_chars.clone().skip(i).collect();
let skipped: String = name_chars.clone().take(i).collect();
if !skipped.contains('/') && match_mailbox_pattern(&rest_name, &rest_pattern) {
return true;
}
}
return false;
}
(Some(&p), Some(&n)) => {
if p == n {
pattern_chars.next();
name_chars.next();
} else {
return false;
}
}
(Some(_), None) => return false,
}
}
}