2025-11-19 02:14:37 +08:00
|
|
|
//
|
2026-03-10 01:32:57 +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/>.
|
|
|
|
|
|
2026-04-23 21:45:34 +08:00
|
|
|
use crate::account::migration::AccountModel;
|
|
|
|
|
use crate::account::state::{DownloadState, FolderStatus};
|
|
|
|
|
use crate::cache::imap::download::flow::{generate_uid_sequence_hashset, DEFAULT_BATCH_SIZE};
|
2026-05-14 02:29:23 +08:00
|
|
|
use crate::cache::imap::mailbox::MailBox;
|
2026-04-23 21:45:34 +08:00
|
|
|
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;
|
2026-05-14 02:29:23 +08:00
|
|
|
use crate::{error::BichonResult, imap::manager::ImapConnectionManager};
|
2026-01-28 20:41:17 +08:00
|
|
|
use async_imap::types::Name;
|
|
|
|
|
use async_imap::Session;
|
2025-11-19 02:14:37 +08:00
|
|
|
use futures::TryStreamExt;
|
|
|
|
|
use std::collections::HashSet;
|
2026-04-15 20:02:41 +08:00
|
|
|
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[])";
|
|
|
|
|
|
2026-01-28 20:41:17 +08:00
|
|
|
pub struct ImapExecutor;
|
2025-11-19 02:14:37 +08:00
|
|
|
|
|
|
|
|
impl ImapExecutor {
|
2026-01-28 20:41:17 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:41:17 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 22:56:06 +08:00
|
|
|
pub async fn append(
|
2026-01-28 20:41:17 +08:00
|
|
|
session: &mut Session<Box<dyn SessionStream>>,
|
2025-12-31 22:56:06 +08:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 02:14:37 +08:00
|
|
|
pub async fn fetch_new_mail(
|
2026-01-28 20:41:17 +08:00
|
|
|
session: &mut Session<Box<dyn SessionStream>>,
|
2025-12-28 13:01:34 +08:00
|
|
|
account: &AccountModel,
|
2025-11-19 02:14:37 +08:00
|
|
|
mailbox: &MailBox,
|
|
|
|
|
start_uid: u64,
|
2025-12-31 22:56:06 +08:00
|
|
|
before: Option<&str>,
|
2026-04-15 20:02:41 +08:00
|
|
|
token: CancellationToken,
|
2025-11-19 02:14:37 +08:00
|
|
|
) -> BichonResult<()> {
|
|
|
|
|
assert!(start_uid > 0, "start_uid must be greater than 0");
|
2025-12-29 12:06:04 +08:00
|
|
|
|
|
|
|
|
let query = match before {
|
|
|
|
|
Some(date) => format!("UID {start_uid}:* BEFORE {date}"),
|
|
|
|
|
None => format!("UID {start_uid}:*"),
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-15 20:02:41 +08:00
|
|
|
let uid_list = match Self::uid_search(session, &mailbox.encoded_name(), &query).await {
|
|
|
|
|
Ok(uid_list) => uid_list,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
let err_msg = format!("UID search failed in [{}]: {:#?}", mailbox.name, e);
|
|
|
|
|
DownloadState::update_folder_progress(
|
|
|
|
|
account.id,
|
|
|
|
|
mailbox.name.clone(),
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
FolderStatus::Failed,
|
|
|
|
|
Some(err_msg.clone()),
|
2026-05-14 02:29:23 +08:00
|
|
|
)?;
|
|
|
|
|
DownloadState::append_session_error(account.id, err_msg)?;
|
2026-04-15 20:02:41 +08:00
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-19 02:14:37 +08:00
|
|
|
|
|
|
|
|
let len = uid_list.len();
|
|
|
|
|
if len == 0 {
|
2026-04-15 20:02:41 +08:00
|
|
|
let msg = match before {
|
|
|
|
|
Some(date) => format!("No emails found before {}.", date),
|
|
|
|
|
None => "No new emails found.".into(),
|
|
|
|
|
};
|
|
|
|
|
DownloadState::update_folder_progress(
|
|
|
|
|
account.id,
|
|
|
|
|
mailbox.name.clone(),
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
FolderStatus::Success,
|
|
|
|
|
Some(msg),
|
2026-05-14 02:29:23 +08:00
|
|
|
)?;
|
2025-11-19 02:14:37 +08:00
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
info!(
|
|
|
|
|
"[account {}][mailbox {}] {} envelopes need to be fetched",
|
2025-12-28 13:01:34 +08:00
|
|
|
account.id, mailbox.name, len
|
2025-11-19 02:14:37 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut uid_vec: Vec<u32> = uid_list.into_iter().collect();
|
|
|
|
|
uid_vec.sort();
|
2025-12-28 13:01:34 +08:00
|
|
|
let uid_batches = generate_uid_sequence_hashset(
|
|
|
|
|
uid_vec,
|
2026-04-16 14:50:27 +08:00
|
|
|
account.download_batch_size.unwrap_or(DEFAULT_BATCH_SIZE) as usize,
|
2025-12-28 13:01:34 +08:00
|
|
|
false,
|
|
|
|
|
);
|
2026-04-15 20:02:41 +08:00
|
|
|
let mut current_processed = 0u64;
|
|
|
|
|
let mut has_error_or_cancel = false;
|
2025-11-19 02:14:37 +08:00
|
|
|
for (index, batch) in uid_batches.into_iter().enumerate() {
|
2026-04-15 20:02:41 +08:00
|
|
|
if token.is_cancelled() {
|
|
|
|
|
break;
|
2025-11-19 02:14:37 +08:00
|
|
|
}
|
2026-04-15 20:02:41 +08:00
|
|
|
match Self::uid_batch_retrieve_emails(
|
2026-01-28 20:41:17 +08:00
|
|
|
session,
|
|
|
|
|
account.id,
|
|
|
|
|
mailbox.id,
|
2026-04-15 20:02:41 +08:00
|
|
|
&batch.0,
|
|
|
|
|
token.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
current_processed += batch.1;
|
|
|
|
|
DownloadState::update_folder_progress(
|
|
|
|
|
account.id,
|
|
|
|
|
mailbox.name.clone(),
|
|
|
|
|
len as u64,
|
|
|
|
|
current_processed,
|
|
|
|
|
FolderStatus::Downloading,
|
|
|
|
|
None,
|
2026-05-14 02:29:23 +08:00
|
|
|
)?;
|
2026-04-15 20:02:41 +08:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
let err_msg = format!("Batch {} failed: {:#?}", index, e);
|
2026-05-14 02:29:23 +08:00
|
|
|
DownloadState::append_session_error(account.id, err_msg.clone())?;
|
2026-04-15 20:02:41 +08:00
|
|
|
DownloadState::update_folder_progress(
|
|
|
|
|
account.id,
|
|
|
|
|
mailbox.name.clone(),
|
|
|
|
|
len as u64,
|
|
|
|
|
current_processed,
|
|
|
|
|
FolderStatus::Failed,
|
|
|
|
|
Some(err_msg),
|
2026-05-14 02:29:23 +08:00
|
|
|
)?;
|
2026-04-15 20:02:41 +08:00
|
|
|
has_error_or_cancel = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !has_error_or_cancel {
|
|
|
|
|
DownloadState::update_folder_progress(
|
|
|
|
|
account.id,
|
|
|
|
|
mailbox.name.clone(),
|
|
|
|
|
len as u64,
|
|
|
|
|
current_processed,
|
|
|
|
|
FolderStatus::Success,
|
|
|
|
|
None,
|
2026-05-14 02:29:23 +08:00
|
|
|
)?;
|
2025-11-19 02:14:37 +08:00
|
|
|
}
|
2026-04-15 20:02:41 +08:00
|
|
|
|
2025-11-19 02:14:37 +08:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn batch_retrieve_emails(
|
2026-01-28 20:41:17 +08:00
|
|
|
session: &mut Session<Box<dyn SessionStream>>,
|
2025-11-19 02:14:37 +08:00
|
|
|
account_id: u64,
|
|
|
|
|
mailbox_id: u64,
|
2026-04-15 20:02:41 +08:00
|
|
|
total: u64,
|
2025-11-19 02:14:37 +08:00
|
|
|
page: u64,
|
|
|
|
|
page_size: u64,
|
|
|
|
|
encoded_mailbox_name: &str,
|
|
|
|
|
desc: bool,
|
2026-04-15 20:02:41 +08:00
|
|
|
token: CancellationToken,
|
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");
|
|
|
|
|
|
|
|
|
|
let (start, end) = if desc {
|
|
|
|
|
// Fetch messages starting from the newest (descending order)
|
|
|
|
|
let end = total.saturating_sub((page - 1) * page_size);
|
|
|
|
|
if end == 0 {
|
|
|
|
|
return Ok(0);
|
|
|
|
|
}
|
|
|
|
|
// Calculate start as end - page_size + 1 to avoid off-by-one errors
|
|
|
|
|
let start = end.saturating_sub(page_size - 1).max(1);
|
|
|
|
|
(start, end)
|
|
|
|
|
} else {
|
|
|
|
|
// Fetch messages starting from the oldest (ascending order)
|
|
|
|
|
let start = (page - 1) * page_size + 1;
|
|
|
|
|
if start > total {
|
|
|
|
|
return Ok(0);
|
|
|
|
|
}
|
|
|
|
|
// Calculate end, capped by the total number of messages
|
|
|
|
|
let end = (start + page_size - 1).min(total);
|
|
|
|
|
(start, end)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let sequence_set = format!("{}:{}", start, end);
|
|
|
|
|
info!(
|
|
|
|
|
"Fetching mailbox '{}' messages: sequence {} (page {}, page_size {}, desc={})",
|
|
|
|
|
encoded_mailbox_name, sequence_set, page, page_size, desc
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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))?
|
|
|
|
|
{
|
2026-04-15 20:02:41 +08:00
|
|
|
if token.is_cancelled() {
|
|
|
|
|
tracing::info!("Account {}: UID fetch stream interrupted.", account_id);
|
|
|
|
|
return Err(raise_error!(
|
|
|
|
|
"Stream cancelled".into(),
|
|
|
|
|
ErrorCode::InternalError
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-01 04:49:18 +08:00
|
|
|
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(
|
2026-01-28 20:41:17 +08:00
|
|
|
session: &mut Session<Box<dyn SessionStream>>,
|
2025-11-19 02:14:37 +08:00
|
|
|
account_id: u64,
|
|
|
|
|
mailbox_id: u64,
|
|
|
|
|
uid_set: &str,
|
2026-04-15 20:02:41 +08:00
|
|
|
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))?
|
|
|
|
|
{
|
2026-04-15 20:02:41 +08:00
|
|
|
if token.is_cancelled() {
|
|
|
|
|
tracing::info!("Account {}: UID fetch stream interrupted.", account_id);
|
|
|
|
|
return Err(raise_error!(
|
|
|
|
|
"Stream cancelled".into(),
|
|
|
|
|
ErrorCode::InternalError
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-01 04:49:18 +08:00
|
|
|
extract_envelope_and_store_it(fetch, account_id, mailbox_id).await?;
|
2025-11-19 02:14:37 +08:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-12-08 03:41:56 +08:00
|
|
|
|
2026-05-22 13:46:40 +02:00
|
|
|
/// 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()
|
|
|
|
|
// {}
|
2026-05-22 13:46:40 +02:00
|
|
|
|
|
|
|
|
Ok(body)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:41:17 +08:00
|
|
|
pub async fn create_connection(
|
|
|
|
|
account_id: u64,
|
|
|
|
|
) -> BichonResult<Session<Box<dyn SessionStream>>> {
|
|
|
|
|
ImapConnectionManager::build(account_id).await
|
2025-12-08 03:41:56 +08:00
|
|
|
}
|
2025-11-19 02:14:37 +08:00
|
|
|
}
|