Files
rustmailer/src/modules/imap/executor.rs
T
2025-07-17 22:49:42 +08:00

544 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::modules::cache::imap::mailbox::EnvelopeFlag;
use crate::modules::error::code::ErrorCode;
use crate::modules::{error::RustMailerResult, imap::manager::ImapConnectionManager};
use crate::raise_error;
use async_imap::types::{Fetch, Mailbox, Name};
use bb8::Pool;
use futures::TryStreamExt;
use std::collections::HashSet;
use std::ops::Add;
use tracing::info;
/// The IMAP query to fetch email metadata including headers and body structure.
const RICH_METADATA_QUERY: &str = "(UID BODYSTRUCTURE RFC822.SIZE INTERNALDATE FLAGS BODY.PEEK[HEADER.FIELDS (BCC CC Date From In-Reply-To Sender Return-Path Message-ID Subject MIME-Version References Reply-To To Received)])";
const MINIMAL_METADATA_QUERY: &str = "(UID FLAGS)";
const UID_FLAGS: &str = "(UID FLAGS)";
const BODYSTRUCTURE: &str = "(UID BODYSTRUCTURE RFC822.SIZE)";
// const SUBSCRIBE_INFO_QUERY: &str =
// "(UID BODY.PEEK[HEADER.FIELDS (List-Unsubscribe List-Subscribe List-ID)])";
const BODY_FETCH_COMMAND: &str = "(BODY.PEEK[])";
pub struct ImapExecutor {
pool: Pool<ImapConnectionManager>,
}
impl ImapExecutor {
pub fn new(pool: Pool<ImapConnectionManager>) -> Self {
Self { pool }
}
pub async fn list_all_mailboxes(&self) -> RustMailerResult<Vec<Name>> {
let mut session = self.pool.get().await?;
let list = session
.list(Some(""), Some("*"))
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Name>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn list_all_subscribed_mailboxes(&self) -> RustMailerResult<Vec<Name>> {
let mut session = self.pool.get().await?;
let list = session
.lsub(Some(""), Some("*"))
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Name>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn create_mailbox(&self, mailbox_name: &str) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.create(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn examine_mailbox(&self, mailbox_name: &str) -> RustMailerResult<Mailbox> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))
}
pub async fn expunge_mailbox(&self, mailbox_name: &str) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.select(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let _ = session
.expunge()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn delete_mailbox(&self, mailbox_name: &str) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.delete(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn rename_mailbox(&self, from: &str, to: &str) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.rename(from, to)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn subscribe_mailbox(&self, mailbox_name: &str) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.subscribe(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn unsubscribe_mailbox(&self, mailbox_name: &str) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.unsubscribe(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn fetch_uid_list(
&self,
start_uid: u32,
mailbox_name: &str,
minimal: bool,
) -> RustMailerResult<Vec<Fetch>> {
assert!(start_uid > 0, "start_uid must be greater than 0");
let uid_set = format!("{}:*", start_uid);
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let list = session
.uid_fetch(
uid_set.as_str(),
if minimal {
MINIMAL_METADATA_QUERY
} else {
"(UID)"
},
)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn retrieve_metadata_paginated(
&self,
page: u64,
page_size: u64,
mailbox_name: &str,
desc: bool,
minimal: bool,
) -> RustMailerResult<(Vec<Fetch>, u64)> {
assert!(page > 0, "Page number must be greater than 0");
assert!(page_size > 0, "Page size must be greater than 0");
let mut session = self.pool.get().await?;
let total = session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.exists as u64;
if total == 0 {
return Ok((Vec::new(), 0));
}
let (start, end) = if desc {
let end = total.saturating_sub((page - 1) * page_size);
let start = end.saturating_sub(page_size).add(1).max(1);
(start, end)
} else {
let start = (page - 1) * page_size + 1;
let end = (start + page_size - 1).min(total);
(start, end)
};
// Return early if the start exceeds total
if start > total || end < 1 || start > end {
return Ok((Vec::new(), total));
}
let sequence_set = format!("{}:{}", start, end);
info!("The sequence range pulled this time is: {}", sequence_set);
let query = if minimal {
MINIMAL_METADATA_QUERY
} else {
RICH_METADATA_QUERY
};
let list = session
.fetch(sequence_set.as_str(), query)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok((result, total))
}
pub async fn retrieve_paginated_uid_and_flags(
&self,
page: u32,
page_size: u32,
mailbox_name: &str,
) -> RustMailerResult<Vec<Fetch>> {
assert!(page > 0, "Page number must be greater than 0");
assert!(page_size > 0, "Page size must be greater than 0");
let mut session = self.pool.get().await?;
let total = session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.exists;
if total == 0 {
return Ok(Vec::new());
}
let start = (page - 1) * page_size + 1;
// Return early if the start exceeds total
if start > total {
return Ok(Vec::new());
}
// Calculate end index, capped at total
let end = (start + page_size - 1).min(total);
// Format and print the sequence set
let sequence_set = format!("{}:{}", start, end);
let list = session
.fetch(sequence_set.as_str(), UID_FLAGS)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn uid_fetch_uid_and_flags(
&self,
uid_set: &str,
mailbox_name: &str,
) -> RustMailerResult<Vec<Fetch>> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let list = session
.uid_fetch(uid_set, UID_FLAGS)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn uid_fetch_body_structure(
&self,
uid_set: &str,
mailbox_name: &str,
) -> RustMailerResult<Vec<Fetch>> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = session
.uid_fetch(uid_set, BODYSTRUCTURE)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn uid_fetch_meta(
&self,
uid_set: &str,
mailbox_name: &str,
minimal: bool,
) -> RustMailerResult<Vec<Fetch>> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let query = if minimal {
MINIMAL_METADATA_QUERY
} else {
RICH_METADATA_QUERY
};
let result = session
.uid_fetch(uid_set, query)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn append(
&self,
mailbox_name: impl AsRef<str>,
flags: Option<&str>,
internaldate: Option<&str>,
content: impl AsRef<[u8]>,
) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.append(mailbox_name, flags, internaldate, content)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))
}
pub async fn uid_fetch_full_message(
&self,
uid: &str,
mailbox_name: &str,
) -> RustMailerResult<Option<Fetch>> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let mut stream = session
.uid_fetch(uid, BODY_FETCH_COMMAND)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let fetch = stream
.try_next()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(fetch)
}
pub async fn uid_fetch_single_part(
&self,
uid: &str,
mailbox_name: &str,
path: &str,
) -> RustMailerResult<Vec<Fetch>> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let list = session
.uid_fetch(uid, &format!("(UID BODY.PEEK[{}])", path))
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
// pub async fn uid_expunge_envelopes(
// &self,
// uid_set: &str,
// mailbox_name: &str,
// ) -> RustMailerResult<()> {
// let mut session = self.pool.get().await?;
// session.examine(mailbox_name).await.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapOperationFailed))?;
// let _ = session.uid_expunge(uid_set).await.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapOperationFailed))?;
// Ok(())
// }
pub async fn uid_move_envelopes(
&self,
uid_set: &str,
from: &str,
to: &str,
) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.select(from)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
session
.uid_mv(uid_set, to)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
pub async fn uid_copy_envelopes(
&self,
uid_set: &str,
from: &str,
to: &str,
) -> RustMailerResult<()> {
let mut session = self.pool.get().await?;
session
.select(from)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
session
.uid_copy(uid_set, to)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(())
}
async fn uid_flag_store(
&self,
uid_set: &str,
mailbox_name: &str,
query: &str,
) -> RustMailerResult<Vec<Fetch>> {
let mut session = self.pool.get().await?;
session
.select(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let list = session
.uid_store(uid_set, query)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = list
.try_collect::<Vec<Fetch>>()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
pub async fn uid_set_flags(
&self,
uid_set: &str,
mailbox_name: &str,
add_flags: Option<Vec<EnvelopeFlag>>,
remove_flags: Option<Vec<EnvelopeFlag>>,
overwrite_flags: Option<Vec<EnvelopeFlag>>,
) -> RustMailerResult<Vec<Fetch>> {
// Validate inputs
if uid_set.is_empty() {
return Err(raise_error!(
"UID set cannot be empty".into(),
ErrorCode::InternalError
));
}
if mailbox_name.is_empty() {
return Err(raise_error!(
"Mailbox name cannot be empty".into(),
ErrorCode::InternalError
));
}
let mut result = Vec::new();
// Helper to convert flags to IMAP string
let flags_to_string = |flags: &[EnvelopeFlag]| -> RustMailerResult<String> {
if flags.is_empty() {
return Err(raise_error!(
"Flag list cannot be empty".into(),
ErrorCode::InternalError
));
}
let flag_strings: Result<Vec<String>, _> =
flags.iter().map(|f| f.to_imap_string()).collect();
Ok(flag_strings?.join(" "))
};
// Process overwrite flags
if let Some(ref flags) = overwrite_flags {
let flags_str = flags_to_string(flags)?;
let res = self
.uid_flag_store(uid_set, mailbox_name, &format!("FLAGS ({})", flags_str))
.await?;
return Ok(res); // Early return for overwrite, as its exclusive
}
// Process add flags
if let Some(ref flags) = add_flags {
let flags_str = flags_to_string(flags)?;
let res = self
.uid_flag_store(uid_set, mailbox_name, &format!("+FLAGS ({})", flags_str))
.await?;
result.extend(res);
}
// Process remove flags
if let Some(ref flags) = remove_flags {
let flags_str = flags_to_string(flags)?;
let res = self
.uid_flag_store(uid_set, mailbox_name, &format!("-FLAGS ({})", flags_str))
.await?;
result.extend(res);
}
Ok(result)
}
pub async fn uid_delete_envelopes(
&self,
uid_set: &str,
mailbox_name: &str,
) -> RustMailerResult<()> {
self.uid_flag_store(uid_set, mailbox_name, "+FLAGS (\\Deleted)")
.await?;
self.expunge_mailbox(mailbox_name).await
}
pub async fn uid_search(
&self,
mailbox_name: &str,
query: &str,
) -> RustMailerResult<HashSet<u32>> {
let mut session = self.pool.get().await?;
session
.examine(mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let result = session
.uid_search(query)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
Ok(result)
}
}