Files
bichon/crates/core/src/imap/executor.rs
T

323 lines
11 KiB
Rust
Raw Normal View History

2025-11-19 02:14:37 +08:00
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
2025-11-19 02:14:37 +08:00
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::account::migration::AccountModel;
2026-05-26 15:23:34 +08:00
use crate::account::state::{DownloadState, DownloadStatus, FolderStatus};
use crate::cache::imap::mailbox::MailBox;
use crate::envelope::extractor::extract_envelope_and_store_it;
use crate::error::code::ErrorCode;
use crate::imap::session::SessionStream;
2025-11-19 02:14:37 +08:00
use crate::raise_error;
use crate::{error::BichonResult, imap::manager::ImapConnectionManager};
use async_imap::types::Name;
use async_imap::Session;
2025-11-19 02:14:37 +08:00
use futures::TryStreamExt;
use std::collections::HashSet;
use tokio_util::sync::CancellationToken;
2025-11-19 02:14:37 +08:00
use tracing::info;
const BODY_FETCH_COMMAND: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[])";
pub struct ImapExecutor;
2025-11-19 02:14:37 +08:00
impl ImapExecutor {
pub async fn list_all_mailboxes(
session: &mut Session<Box<dyn SessionStream>>,
) -> BichonResult<Vec<Name>> {
2025-11-19 02:14:37 +08:00
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 uid_search(
session: &mut Session<Box<dyn SessionStream>>,
mailbox_name: &str,
query: &str,
) -> BichonResult<HashSet<u32>> {
2025-11-19 02:14:37 +08:00
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)
}
pub async fn append(
session: &mut Session<Box<dyn SessionStream>>,
mailbox_name: impl AsRef<str>,
flags: Option<&str>,
internaldate: Option<&str>,
content: impl AsRef<[u8]>,
) -> BichonResult<()> {
session
.append(mailbox_name, flags, internaldate, content)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))
}
2026-05-26 15:23:34 +08:00
/// Fetches new mail for a mailbox by directly issuing a ranged UID FETCH.
///
/// Unlike the old two-step approach (UID SEARCH → batch UID FETCH),
/// this sends a single `UID FETCH {start}:*` and streams the results,
/// eliminating one IMAP round-trip.
///
/// Returns `Ok(Some(max_uid))` with the highest UID fetched, or `Ok(None)`
/// if no new mail was found.
2025-11-19 02:14:37 +08:00
pub async fn fetch_new_mail(
session: &mut Session<Box<dyn SessionStream>>,
account: &AccountModel,
2025-11-19 02:14:37 +08:00
mailbox: &MailBox,
start_uid: u64,
before: Option<&str>,
token: CancellationToken,
2026-05-26 15:23:34 +08:00
) -> BichonResult<Option<u32>> {
2025-11-19 02:14:37 +08:00
assert!(start_uid > 0, "start_uid must be greater than 0");
2026-05-26 15:23:34 +08:00
// Select the mailbox (read-only) so UID FETCH works.
session
.examine(&mailbox.encoded_name())
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
// Build the UID range. IMAP UID FETCH accepts the same range syntax
// as UID SEARCH, so we can skip the separate SEARCH round-trip.
let uid_range = match before {
Some(date) => format!("{start_uid}:* BEFORE {date}"),
None => format!("{start_uid}:*"),
};
2026-05-26 15:23:34 +08:00
info!(
"[account {}][mailbox {}] fetch_new_mail: direct UID FETCH {}",
account.id, mailbox.name, uid_range
);
let mut stream = session
.uid_fetch(&uid_range, BODY_FETCH_COMMAND)
.await
.map_err(|e| {
let err_msg = format!(
"UID FETCH failed in [{}]: {:#?}",
mailbox.name, e
);
let _ = DownloadState::append_session_error(account.id, err_msg);
raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)
})?;
let mut count = 0u64;
let mut max_uid: Option<u32> = None;
while let Some(fetch) = stream
.try_next()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
{
if token.is_cancelled() {
tracing::info!(
"Account {}: fetch_new_mail stream interrupted.",
account.id
);
DownloadState::update_session_status(
account.id,
2026-05-26 15:23:34 +08:00
DownloadStatus::Cancelled,
Some("User stopped or system shutdown".to_string()),
)?;
2026-05-26 15:23:34 +08:00
return Err(raise_error!(
"Stream cancelled".into(),
ErrorCode::InternalError
));
}
2025-11-19 02:14:37 +08:00
2026-05-26 15:23:34 +08:00
if let Some(uid) = fetch.uid {
max_uid = Some(max_uid.unwrap_or(0).max(uid));
}
extract_envelope_and_store_it(fetch, account.id, mailbox.id).await?;
count += 1;
}
if count == 0 {
DownloadState::update_folder_progress(
account.id,
mailbox.name.clone(),
0,
0,
FolderStatus::Success,
2026-05-26 15:23:34 +08:00
Some("No new emails found.".into()),
)?;
2026-05-26 15:23:34 +08:00
} else {
DownloadState::update_folder_progress(
account.id,
mailbox.name.clone(),
2026-05-26 15:23:34 +08:00
count,
count,
FolderStatus::Success,
None,
)?;
2025-11-19 02:14:37 +08:00
}
2026-05-26 15:23:34 +08:00
Ok(max_uid)
2025-11-19 02:14:37 +08:00
}
pub async fn batch_retrieve_emails(
session: &mut Session<Box<dyn SessionStream>>,
2025-11-19 02:14:37 +08:00
account_id: u64,
mailbox_id: u64,
total: u64,
2025-11-19 02:14:37 +08:00
page: u64,
page_size: u64,
encoded_mailbox_name: &str,
token: CancellationToken,
2026-05-26 15:23:34 +08:00
max_uid: &mut Option<u32>,
2025-11-19 02:14:37 +08:00
) -> BichonResult<usize> {
assert!(page > 0, "Page number must be greater than 0");
assert!(page_size > 0, "Page size must be greater than 0");
2026-05-26 15:23:34 +08:00
// Fetch messages starting from the oldest (ascending order).
let start = (page - 1) * page_size + 1;
if start > total {
return Ok(0);
}
let end = (start + page_size - 1).min(total);
2025-11-19 02:14:37 +08:00
let sequence_set = format!("{}:{}", start, end);
info!(
2026-05-26 15:23:34 +08:00
"Fetching mailbox '{}' messages: sequence {} (page {}, page_size {})",
encoded_mailbox_name, sequence_set, page, page_size
2025-11-19 02:14:37 +08:00
);
let mut stream = session
.fetch(sequence_set.as_str(), BODY_FETCH_COMMAND)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let mut count = 0;
while let Some(fetch) = stream
.try_next()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
{
if token.is_cancelled() {
tracing::info!("Account {}: UID fetch stream interrupted.", account_id);
return Err(raise_error!(
"Stream cancelled".into(),
ErrorCode::InternalError
));
}
2026-05-26 15:23:34 +08:00
if let Some(uid) = fetch.uid {
*max_uid = Some((*max_uid).unwrap_or(0).max(uid));
}
extract_envelope_and_store_it(fetch, account_id, mailbox_id).await?;
2025-11-19 02:14:37 +08:00
count += 1;
}
Ok(count)
}
pub async fn uid_batch_retrieve_emails(
session: &mut Session<Box<dyn SessionStream>>,
2025-11-19 02:14:37 +08:00
account_id: u64,
mailbox_id: u64,
uid_set: &str,
token: CancellationToken,
2025-11-19 02:14:37 +08:00
) -> BichonResult<()> {
let mut stream = session
.uid_fetch(uid_set, BODY_FETCH_COMMAND)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
while let Some(fetch) = stream
.try_next()
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
{
if token.is_cancelled() {
tracing::info!("Account {}: UID fetch stream interrupted.", account_id);
return Err(raise_error!(
"Stream cancelled".into(),
ErrorCode::InternalError
));
}
extract_envelope_and_store_it(fetch, account_id, mailbox_id).await?;
2025-11-19 02:14:37 +08:00
}
Ok(())
}
/// Fetches the raw RFC822 body of a single message by UID.
///
/// Selects (read-only) the given mailbox and issues `UID FETCH <uid> (BODY.PEEK[])`.
/// Used for on-demand self-healing when an indexed message's content blob is missing.
/// Returns the raw bytes, or an error if the message cannot be retrieved.
pub async fn fetch_single_message_body(
session: &mut Session<Box<dyn SessionStream>>,
encoded_mailbox_name: &str,
uid: u32,
) -> BichonResult<Vec<u8>> {
session
.examine(encoded_mailbox_name)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?;
let mut stream = session
.uid_fetch(uid.to_string(), 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_or_else(|| {
raise_error!(
format!("UID {uid} not found on IMAP server"),
ErrorCode::ResourceNotFound
)
})?;
let body = fetch
.body()
.ok_or_else(|| {
raise_error!(
format!("No body returned for UID {uid}"),
ErrorCode::ImapUnexpectedResult
)
})?
.to_vec();
2026-05-23 12:08:49 +08:00
// // Drain any remaining items so the stream is fully consumed before reuse.
// while stream
// .try_next()
// .await
// .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
// .is_some()
// {}
Ok(body)
}
pub async fn create_connection(
account_id: u64,
) -> BichonResult<Session<Box<dyn SessionStream>>> {
ImapConnectionManager::build(account_id).await
}
2025-11-19 02:14:37 +08:00
}