mirror of
https://github.com/rustmailer/rustmailer.git
synced 2026-08-21 08:01:29 +00:00
refactor(gmail): update account sync logic and related files
This commit is contained in:
@@ -6,7 +6,7 @@ use crate::{
|
||||
modules::error::{code::ErrorCode, RustMailerResult},
|
||||
raise_error,
|
||||
};
|
||||
use chrono::{Datelike, Days, Local, Months, NaiveDate, TimeZone, Utc};
|
||||
use chrono::{Datelike, Days, Local, Months, NaiveDate, Utc};
|
||||
use poem_openapi::{Enum, Object};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -73,7 +73,7 @@ impl AddressEntity {
|
||||
}
|
||||
|
||||
pub async fn clean_account(account_id: u64) -> RustMailerResult<()> {
|
||||
const BATCH_SIZE: usize = 500;
|
||||
const BATCH_SIZE: usize = 200;
|
||||
let mut total_deleted = 0usize;
|
||||
let start_time = Instant::now();
|
||||
loop {
|
||||
|
||||
Vendored
+2
-2
@@ -68,7 +68,7 @@ impl MinimalEnvelope {
|
||||
}
|
||||
|
||||
pub async fn clean_mailbox_envelopes(account_id: u64, mailbox_id: u64) -> RustMailerResult<()> {
|
||||
const BATCH_SIZE: usize = 500;
|
||||
const BATCH_SIZE: usize = 200;
|
||||
let mut total_deleted = 0usize;
|
||||
let start_time = Instant::now();
|
||||
loop {
|
||||
@@ -162,7 +162,7 @@ impl MinimalEnvelope {
|
||||
}
|
||||
|
||||
pub async fn clean_account(account_id: u64) -> RustMailerResult<()> {
|
||||
const BATCH_SIZE: usize = 500;
|
||||
const BATCH_SIZE: usize = 200;
|
||||
let mut total_deleted = 0usize;
|
||||
let start_time = Instant::now();
|
||||
loop {
|
||||
|
||||
Vendored
+43
-28
@@ -4,6 +4,7 @@
|
||||
|
||||
use crate::modules::account::entity::{AuthType, MailerType};
|
||||
use crate::modules::cache::imap::sync::execute_imap_sync;
|
||||
use crate::modules::cache::vendor::gmail::sync::execute_gmail_sync;
|
||||
use crate::modules::oauth2::token::OAuth2AccessToken;
|
||||
use crate::modules::scheduler::periodic::TaskHandle;
|
||||
use crate::modules::{
|
||||
@@ -19,7 +20,7 @@ use tracing::{error, warn};
|
||||
|
||||
static _DESCRIPTION: &str = "This task periodically synchronizes mailbox data for a specified account, ensuring that all local data is up-to-date.";
|
||||
const TASK_INTERVAL: Duration = Duration::from_secs(10);
|
||||
pub static IMAP_TASKS: LazyLock<AccountSyncTask> = LazyLock::new(AccountSyncTask::new);
|
||||
pub static SYNC_TASKS: LazyLock<AccountSyncTask> = LazyLock::new(AccountSyncTask::new);
|
||||
static LAST_WARN_TIME: AtomicI64 = AtomicI64::new(0);
|
||||
const WARN_INTERVAL_MS: i64 = 600_000;
|
||||
|
||||
@@ -34,7 +35,7 @@ impl AccountSyncTask {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_account_task(&self, account_id: u64, email: String) {
|
||||
pub async fn start_account_sync_task(&self, account_id: u64, email: String) {
|
||||
let task_name = format!("account-sync-task-{}-{}", account_id, &email);
|
||||
let periodic_task = PeriodicTask::new(&task_name);
|
||||
let task = move |param: Option<u64>| {
|
||||
@@ -54,34 +55,48 @@ impl AccountSyncTask {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if matches!(account.mailer_type, MailerType::ImapSmtp) {
|
||||
if let AuthType::OAuth2 = account
|
||||
.imap
|
||||
.as_ref()
|
||||
.expect(
|
||||
"BUG: account.imap is None, but this should never happen here",
|
||||
)
|
||||
.auth
|
||||
.auth_type
|
||||
{
|
||||
if OAuth2AccessToken::get(account.id).await?.is_none() {
|
||||
if utc_now!() % 300_000 == 0 {
|
||||
warn!("Account {}: Sync aborted. OAuth2 authorization not completed. Please visit the rustmailer admin page to authorize this account.", account_id);
|
||||
match account.mailer_type {
|
||||
MailerType::ImapSmtp => {
|
||||
if let AuthType::OAuth2 = account.imap.as_ref().expect("BUG: account.imap is None, but this should never happen here").auth.auth_type {
|
||||
if OAuth2AccessToken::get(account.id).await?.is_none() {
|
||||
if utc_now!() % 300_000 == 0 {
|
||||
warn!("Account {}: Sync aborted. OAuth2 authorization not completed. Please visit the rustmailer admin page to authorize this account.", account_id);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if let Err(e) = execute_imap_sync(&account).await {
|
||||
STATUS_DISPATCHER
|
||||
.append_error(
|
||||
account_id,
|
||||
format!("error in account sync task: {:#?}", e),
|
||||
if let Err(e) = execute_imap_sync(&account).await {
|
||||
STATUS_DISPATCHER
|
||||
.append_error(
|
||||
account_id,
|
||||
format!("error in account sync task: {:#?}", e),
|
||||
)
|
||||
.await;
|
||||
error!(
|
||||
"Failed to synchronize mailbox data for '{}': {:?}",
|
||||
account_id, e
|
||||
)
|
||||
.await;
|
||||
error!(
|
||||
"Failed to synchronize mailbox data for '{}': {:?}",
|
||||
account_id, e
|
||||
)
|
||||
}
|
||||
}
|
||||
MailerType::GmailApi => {
|
||||
if OAuth2AccessToken::get(account.id).await?.is_none() {
|
||||
if utc_now!() % 300_000 == 0 {
|
||||
warn!("Account {}: Sync aborted. OAuth2 authorization not completed. Please visit the rustmailer admin page to authorize this account.", account_id);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(e) = execute_gmail_sync(&account).await {
|
||||
STATUS_DISPATCHER
|
||||
.append_error(
|
||||
account_id,
|
||||
format!("error in account sync task: {:#?}", e),
|
||||
)
|
||||
.await;
|
||||
error!(
|
||||
"Failed to synchronize mailbox data for '{}': {:?}",
|
||||
account_id, e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ impl GmailClient {
|
||||
max_results: u32,
|
||||
) -> RustMailerResult<HistoryList> {
|
||||
let mut url = format!(
|
||||
"https://gmail.googleapis.com/gmail/v1/users/me/history?labelIds={}&maxResults={}&startHistoryId={}",
|
||||
"https://gmail.googleapis.com/gmail/v1/users/me/history?labelId={}&maxResults={}&startHistoryId={}",
|
||||
label_id, max_results, start_history_id
|
||||
);
|
||||
|
||||
|
||||
+37
-3
@@ -2,8 +2,6 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
calculate_hash, id,
|
||||
modules::{
|
||||
@@ -22,10 +20,12 @@ use crate::{
|
||||
},
|
||||
raise_error,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use native_db::*;
|
||||
use native_model::{native_model, Model};
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
@@ -174,10 +174,10 @@ impl GmailEnvelope {
|
||||
Some(e.internal_date),
|
||||
e.date,
|
||||
);
|
||||
|
||||
// --- Store envelope ---
|
||||
rw.insert::<GmailEnvelope>(e)
|
||||
.map_err(|err| raise_error!(format!("{:#?}", err), ErrorCode::InternalError))?;
|
||||
|
||||
// --- Thread upsert ---
|
||||
match rw
|
||||
.get()
|
||||
@@ -261,6 +261,40 @@ impl GmailEnvelope {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clean_account(account_id: u64) -> RustMailerResult<()> {
|
||||
const BATCH_SIZE: usize = 200;
|
||||
let mut total_deleted = 0usize;
|
||||
let start_time = Instant::now();
|
||||
loop {
|
||||
let deleted = batch_delete_impl(DB_MANAGER.envelope_db(), move |rw| {
|
||||
let to_delete: Vec<GmailEnvelope> = rw
|
||||
.scan()
|
||||
.secondary(GmailEnvelopeKey::account_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.start_with(account_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.take(BATCH_SIZE)
|
||||
.try_collect()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(to_delete)
|
||||
})
|
||||
.await?;
|
||||
total_deleted += deleted;
|
||||
// If this batch is empty, break the loop
|
||||
if deleted == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Finished deleting gmail envelopes for account_id={} total_deleted={} in {:?}",
|
||||
account_id,
|
||||
total_deleted,
|
||||
start_time.elapsed()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GmailEnvelope> for EmailEnvelopeV3 {
|
||||
|
||||
+54
-24
@@ -19,7 +19,7 @@ use crate::{
|
||||
raise_error,
|
||||
};
|
||||
|
||||
const ENVELOPE_BATCH_SIZE: u32 = 500;
|
||||
const ENVELOPE_BATCH_SIZE: u32 = 100;
|
||||
|
||||
pub async fn fetch_and_save_since_date(
|
||||
account: &AccountV2,
|
||||
@@ -37,7 +37,7 @@ pub async fn fetch_and_save_since_date(
|
||||
let mut page_token: Option<String> = None;
|
||||
let mut page = 1; // Used only for tracking sync progress
|
||||
let semaphore = Arc::new(Semaphore::new(10));
|
||||
let mut max_history_id = None;
|
||||
let mut history_ids = Vec::new();
|
||||
loop {
|
||||
let resp = GmailClient::list_messages(
|
||||
account_id,
|
||||
@@ -109,7 +109,10 @@ pub async fn fetch_and_save_since_date(
|
||||
})
|
||||
.collect::<RustMailerResult<Vec<GmailEnvelope>>>()?;
|
||||
inserted_count += envelopes.len();
|
||||
max_history_id = compute_max_history_id(&envelopes);
|
||||
let hid = compute_max_history_id(&envelopes);
|
||||
if let Some(hid) = hid {
|
||||
history_ids.push(hid.to_string());
|
||||
}
|
||||
GmailEnvelope::save_envelopes(envelopes).await?;
|
||||
}
|
||||
// Break if API response has no next page
|
||||
@@ -118,7 +121,8 @@ pub async fn fetch_and_save_since_date(
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
Ok((inserted_count, max_history_id))
|
||||
let hid = max_history_id(&history_ids).map(|s| s.to_string());
|
||||
Ok((inserted_count, hid))
|
||||
}
|
||||
|
||||
pub async fn fetch_and_save_full_label(
|
||||
@@ -145,8 +149,8 @@ pub async fn fetch_and_save_full_label(
|
||||
// Each page returns message IDs, and we still need to fetch message details individually.
|
||||
let mut page_token: Option<String> = None;
|
||||
let mut page = 1; // Used only for tracking sync progress
|
||||
let semaphore = Arc::new(Semaphore::new(10));
|
||||
let mut max_history_id = None;
|
||||
let semaphore = Arc::new(Semaphore::new(5));
|
||||
let mut history_ids = Vec::new();
|
||||
loop {
|
||||
let resp = GmailClient::list_messages(
|
||||
account_id,
|
||||
@@ -205,7 +209,10 @@ pub async fn fetch_and_save_full_label(
|
||||
})
|
||||
.collect::<RustMailerResult<Vec<GmailEnvelope>>>()?;
|
||||
inserted_count += envelopes.len();
|
||||
max_history_id = compute_max_history_id(&envelopes);
|
||||
let hid = compute_max_history_id(&envelopes);
|
||||
if let Some(hid) = hid {
|
||||
history_ids.push(hid.to_string());
|
||||
}
|
||||
GmailEnvelope::save_envelopes(envelopes).await?;
|
||||
}
|
||||
// Break if API response has no next page
|
||||
@@ -214,45 +221,68 @@ pub async fn fetch_and_save_full_label(
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
Ok((inserted_count, max_history_id))
|
||||
let hid = max_history_id(&history_ids).map(|s| s.to_string());
|
||||
Ok((inserted_count, hid))
|
||||
}
|
||||
|
||||
fn max_history_id_fallback(a: &str, b: &str) -> String {
|
||||
// Try to parse as u64
|
||||
fn max_history_id_fallback<'a>(a: &'a str, b: &'a str) -> &'a str {
|
||||
match (a.parse::<u64>(), b.parse::<u64>()) {
|
||||
(Ok(a_num), Ok(b_num)) => {
|
||||
if a_num >= b_num {
|
||||
a.to_string()
|
||||
a
|
||||
} else {
|
||||
b.to_string()
|
||||
b
|
||||
}
|
||||
}
|
||||
// If parsing fails, fall back to length + lexicographical comparison
|
||||
_ => {
|
||||
if a.len() > b.len() {
|
||||
a.to_string()
|
||||
a
|
||||
} else if b.len() > a.len() {
|
||||
b.to_string()
|
||||
b
|
||||
} else if a >= b {
|
||||
a
|
||||
} else {
|
||||
// Same length, compare lexicographically
|
||||
if a >= b {
|
||||
a.to_string()
|
||||
} else {
|
||||
b.to_string()
|
||||
}
|
||||
b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_max_history_id(envelopes: &[GmailEnvelope]) -> Option<String> {
|
||||
pub fn max_history_id(ids: &[String]) -> Option<&str> {
|
||||
ids.iter()
|
||||
.map(|s| s.as_str())
|
||||
.reduce(|a, b| max_history_id_fallback(a, b))
|
||||
}
|
||||
|
||||
fn compute_max_history_id<'a>(envelopes: &'a [GmailEnvelope]) -> Option<&'a str> {
|
||||
envelopes
|
||||
.iter()
|
||||
.map(|e| e.history_id.as_str())
|
||||
.fold(None, |max_id, curr| {
|
||||
Some(match max_id {
|
||||
Some(m) => max_history_id_fallback(m.as_str(), curr),
|
||||
None => curr.to_string(),
|
||||
Some(m) => max_history_id_fallback(m, curr),
|
||||
None => curr,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::modules::cache::vendor::gmail::sync::flow::max_history_id_fallback;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test1() {
|
||||
let ids = vec![
|
||||
"2671855", "2671863", "2671871", "2671881", "2671891", "2671898", "100865", "81974",
|
||||
"81967", "2671905", "531772", "531769", "3296", "1385924",
|
||||
];
|
||||
|
||||
let max_id = ids
|
||||
.iter()
|
||||
.cloned()
|
||||
.reduce(|a, b| max_history_id_fallback(a, b))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(max_id, "2671905");
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -2,7 +2,6 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
|
||||
use ahash::{AHashSet, HashSet};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{info, warn};
|
||||
@@ -16,6 +15,7 @@ use crate::{
|
||||
cleanup_single_label,
|
||||
client::GmailClient,
|
||||
envelope::GmailEnvelope,
|
||||
flow::max_history_id,
|
||||
labels::{GmailCheckPoint, GmailLabels},
|
||||
rebuild::rebuild_single_label_cache,
|
||||
},
|
||||
@@ -33,8 +33,9 @@ pub async fn handle_history(
|
||||
let account_id = account.id;
|
||||
let use_proxy = account.use_proxy.clone();
|
||||
let remote_labels = find_existing_remote_labels(local_labels, remote_labels);
|
||||
let checkpoint = GmailCheckPoint::get(account_id).await?;
|
||||
let mut history_ids = Vec::with_capacity(remote_labels.len());
|
||||
for remote in remote_labels {
|
||||
let checkpoint = GmailCheckPoint::get(remote.id).await?;
|
||||
let mut page_token = None;
|
||||
loop {
|
||||
let mut list = match GmailClient::list_history(
|
||||
@@ -55,8 +56,11 @@ pub async fn handle_history(
|
||||
code,
|
||||
} => {
|
||||
if code == ErrorCode::GmailApiInvalidHistoryId {
|
||||
handle_invalid_history_id(account, &remote).await?;
|
||||
continue;
|
||||
let history_id = handle_invalid_history_id(account, &remote).await?;
|
||||
if let Some(history_id) = history_id {
|
||||
history_ids.push(history_id);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
return Err(raise_error!(message, code));
|
||||
}
|
||||
@@ -70,17 +74,19 @@ pub async fn handle_history(
|
||||
.into_iter()
|
||||
.filter(|h| h.has_changes())
|
||||
.collect();
|
||||
|
||||
apply_history(account_id, use_proxy, &remote, history_list).await?;
|
||||
if page_token.is_none() {
|
||||
GmailCheckPoint::new(account_id, remote.id, list.history_id)
|
||||
.save()
|
||||
.await?;
|
||||
history_ids.push(list.history_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
GmailLabels::upsert(remote).await?;
|
||||
}
|
||||
let max = max_history_id(&history_ids);
|
||||
if let Some(history_id) = max {
|
||||
let checkpoint = GmailCheckPoint::new(account_id, history_id.to_string());
|
||||
checkpoint.save().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -214,7 +220,7 @@ pub async fn apply_history(
|
||||
async fn handle_invalid_history_id(
|
||||
account: &AccountV2,
|
||||
label: &GmailLabels,
|
||||
) -> RustMailerResult<()> {
|
||||
) -> RustMailerResult<Option<String>> {
|
||||
info!(
|
||||
"Account {}: Invalid history ID detected for label '{}'. Rebuilding local state...",
|
||||
account.id, label.name
|
||||
@@ -229,11 +235,5 @@ async fn handle_invalid_history_id(
|
||||
"Account {}: Upserted label '{}' into local database",
|
||||
account.id, label.name
|
||||
);
|
||||
rebuild_single_label_cache(account, label).await?;
|
||||
info!(
|
||||
"Account {}: Rebuilt local cache for label '{}'",
|
||||
account.id, label.name
|
||||
);
|
||||
|
||||
Ok(())
|
||||
rebuild_single_label_cache(account, label).await
|
||||
}
|
||||
|
||||
+41
-44
@@ -13,7 +13,7 @@ use crate::{
|
||||
cache::imap::mailbox::MailBox,
|
||||
database::{
|
||||
async_find_impl, batch_delete_impl, batch_insert_impl, delete_impl,
|
||||
filter_by_secondary_key_impl, insert_impl, manager::DB_MANAGER, upsert_impl,
|
||||
filter_by_secondary_key_impl, manager::DB_MANAGER, upsert_impl,
|
||||
},
|
||||
error::{code::ErrorCode, RustMailerResult},
|
||||
},
|
||||
@@ -37,10 +37,6 @@ pub struct GmailLabels {
|
||||
}
|
||||
|
||||
impl GmailLabels {
|
||||
pub async fn save(&self) -> RustMailerResult<()> {
|
||||
insert_impl(DB_MANAGER.envelope_db(), self.to_owned()).await
|
||||
}
|
||||
|
||||
pub async fn upsert(label: GmailLabels) -> RustMailerResult<()> {
|
||||
upsert_impl(DB_MANAGER.envelope_db(), label).await
|
||||
}
|
||||
@@ -85,6 +81,22 @@ impl GmailLabels {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clean(account_id: u64) -> RustMailerResult<()> {
|
||||
batch_delete_impl(DB_MANAGER.envelope_db(), move |rw| {
|
||||
let labels: Vec<GmailLabels> = rw
|
||||
.scan()
|
||||
.secondary::<GmailLabels>(GmailLabelsKey::account_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.start_with(account_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.try_collect()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(labels)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GmailLabels> for MailBox {
|
||||
@@ -110,14 +122,8 @@ impl From<GmailLabels> for MailBox {
|
||||
#[native_model(id = 8, version = 1)]
|
||||
#[native_db]
|
||||
pub struct GmailCheckPoint {
|
||||
/// Primary key for the checkpoint.
|
||||
/// Generated by hashing the combination of `account_id` and `label_id` (as a string).
|
||||
/// Uniquely identifies the sync state of a specific label for a specific account.
|
||||
#[primary_key]
|
||||
pub id: u64,
|
||||
|
||||
/// The Gmail account ID this checkpoint belongs to.
|
||||
#[secondary_key]
|
||||
#[primary_key]
|
||||
pub account_id: u64,
|
||||
|
||||
/// The latest Gmail `historyId` for incremental synchronization.
|
||||
@@ -127,30 +133,28 @@ pub struct GmailCheckPoint {
|
||||
/// Creation timestamp in UNIX epoch milliseconds.
|
||||
/// Records when this checkpoint was initially created.
|
||||
pub created_at: i64,
|
||||
|
||||
/// Last update timestamp in UNIX epoch milliseconds.
|
||||
/// Records the most recent time this checkpoint was updated.
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
impl GmailCheckPoint {
|
||||
pub async fn get(id: u64) -> RustMailerResult<GmailCheckPoint> {
|
||||
let entity = async_find_impl(DB_MANAGER.envelope_db(), id).await?;
|
||||
pub async fn get(account_id: u64) -> RustMailerResult<GmailCheckPoint> {
|
||||
let entity = async_find_impl(DB_MANAGER.envelope_db(), account_id).await?;
|
||||
entity.ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!("GmailCheckPoint not found for id={}", id),
|
||||
format!("GmailCheckPoint not found for id={}", account_id),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(account_id: u64, label_id: u64, max_history_id: String) -> Self {
|
||||
pub async fn find(account_id: u64) -> RustMailerResult<Option<GmailCheckPoint>> {
|
||||
async_find_impl(DB_MANAGER.envelope_db(), account_id).await
|
||||
}
|
||||
|
||||
pub fn new(account_id: u64, history_id: String) -> Self {
|
||||
Self {
|
||||
id: label_id,
|
||||
account_id,
|
||||
history_id: max_history_id,
|
||||
history_id,
|
||||
created_at: utc_now!(),
|
||||
updated_at: utc_now!(),
|
||||
}
|
||||
}
|
||||
// Upsert is used here to overwrite the existing record
|
||||
@@ -158,28 +162,21 @@ impl GmailCheckPoint {
|
||||
upsert_impl(DB_MANAGER.envelope_db(), self.to_owned()).await
|
||||
}
|
||||
|
||||
pub async fn list_all(account_id: u64) -> RustMailerResult<Vec<GmailLabels>> {
|
||||
filter_by_secondary_key_impl(
|
||||
DB_MANAGER.envelope_db(),
|
||||
GmailLabelsKey::account_id,
|
||||
account_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn clean(account_id: u64) -> RustMailerResult<()> {
|
||||
batch_delete_impl(DB_MANAGER.envelope_db(), move |rw| {
|
||||
let to_delete: Vec<GmailCheckPoint> = rw
|
||||
.scan()
|
||||
.secondary(GmailCheckPointKey::account_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.start_with(account_id)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
|
||||
.try_collect()
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(to_delete)
|
||||
})
|
||||
.await?;
|
||||
if Self::find(account_id).await?.is_some() {
|
||||
delete_impl(DB_MANAGER.envelope_db(), move |rw| {
|
||||
rw.get()
|
||||
.primary::<GmailCheckPoint>(account_id)
|
||||
.map_err(|e| raise_error!(e.to_string(), ErrorCode::InternalError))?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"gmail history id checkpoint missing".into(),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+28
-18
@@ -64,12 +64,9 @@ pub async fn execute_gmail_sync(account: &AccountV2) -> RustMailerResult<()> {
|
||||
.collect();
|
||||
|
||||
let local_labels = GmailLabels::list_all(account.id).await?;
|
||||
// How to determine if a rebuild is needed?
|
||||
// Simplified rule: if the local label does not exist, trigger a rebuild.
|
||||
// We do not check how many local message metadata entries exist,
|
||||
// since that would be expensive.
|
||||
let local_checkpoints = GmailCheckPoint::list_all(account.id).await?;
|
||||
if should_rebuild_cache(account, local_labels.len(), local_checkpoints.len()).await? {
|
||||
|
||||
let checkpoint = GmailCheckPoint::find(account.id).await?;
|
||||
if should_rebuild_cache(account, &local_labels, checkpoint).await? {
|
||||
AccountRunningState::set_initial_sync_folders(
|
||||
account.id,
|
||||
remote_labels
|
||||
@@ -78,7 +75,6 @@ pub async fn execute_gmail_sync(account: &AccountV2) -> RustMailerResult<()> {
|
||||
.collect(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
match &account.date_since {
|
||||
Some(date_since) => {
|
||||
rebuild_cache_since_date(account, &remote_labels, date_since).await?;
|
||||
@@ -120,8 +116,14 @@ pub async fn execute_gmail_sync(account: &AccountV2) -> RustMailerResult<()> {
|
||||
}
|
||||
|
||||
if !missing_labels.is_empty() {
|
||||
info!(
|
||||
count = missing_labels.len(),
|
||||
labels = ?missing_labels,
|
||||
"Inserting missing Gmail labels into database"
|
||||
);
|
||||
GmailLabels::batch_insert(&missing_labels).await?;
|
||||
for label in &missing_labels {
|
||||
//During incremental synchronization, if any labels are found missing or not fully synchronized, the checkpoint does not need to be updated.
|
||||
rebuild_single_label_cache(account, label).await?;
|
||||
}
|
||||
}
|
||||
@@ -130,24 +132,32 @@ pub async fn execute_gmail_sync(account: &AccountV2) -> RustMailerResult<()> {
|
||||
|
||||
pub async fn should_rebuild_cache(
|
||||
account: &AccountV2,
|
||||
local_labels_count: usize,
|
||||
local_checkpoints_count: usize,
|
||||
local_labels: &[GmailLabels],
|
||||
checkpoint: Option<GmailCheckPoint>,
|
||||
) -> RustMailerResult<bool> {
|
||||
// If both local labels and checkpoint exist, no rebuild is needed.
|
||||
if local_labels_count > 0 && local_checkpoints_count > 0 {
|
||||
if !local_labels.is_empty() && checkpoint.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
// If there are local mailboxes but no checkpoints, clear the mailboxes.
|
||||
if local_labels_count > 0 {
|
||||
let mailboxes = GmailLabels::list_all(account.id).await?;
|
||||
GmailLabels::batch_delete(mailboxes).await?;
|
||||
|
||||
info!(
|
||||
account_id = account.id,
|
||||
label_count = local_labels.len(),
|
||||
"Rebuilding cache: cleaning local labels and checkpoints"
|
||||
);
|
||||
|
||||
if !local_labels.is_empty() {
|
||||
GmailLabels::batch_delete(local_labels.to_vec()).await?;
|
||||
}
|
||||
if local_checkpoints_count > 0 {
|
||||
//这个要清理,清理掉本地缓存的所有信息,包括关联的索引信息,比如thread, checkpoint也是
|
||||
//EnvelopeFlagsManager::clean_account(account.id).await?
|
||||
if checkpoint.is_some() {
|
||||
GmailCheckPoint::clean(account.id).await?;
|
||||
}
|
||||
// If either remote mailboxes or local envelopes were missing, cache rebuild is required.
|
||||
GmailEnvelope::clean_account(account.id).await?;
|
||||
AddressEntity::clean_account(account.id).await?;
|
||||
EmailThread::clean_account(account.id).await?;
|
||||
|
||||
info!(account_id = account.id, "Cache cleaning completed");
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
+26
-26
@@ -4,9 +4,9 @@
|
||||
|
||||
use crate::modules::{
|
||||
account::{since::DateSince, v2::AccountV2},
|
||||
cache::{
|
||||
vendor::gmail::sync::flow::{fetch_and_save_full_label, fetch_and_save_since_date},
|
||||
vendor::gmail::sync::labels::{GmailCheckPoint, GmailLabels},
|
||||
cache::vendor::gmail::sync::{
|
||||
flow::{fetch_and_save_full_label, fetch_and_save_since_date, max_history_id},
|
||||
labels::{GmailCheckPoint, GmailLabels},
|
||||
},
|
||||
error::RustMailerResult,
|
||||
};
|
||||
@@ -21,6 +21,8 @@ pub async fn rebuild_cache(
|
||||
let mut total_inserted = 0;
|
||||
|
||||
GmailLabels::batch_insert(remote_labels).await?;
|
||||
let mut history_ids = Vec::with_capacity(remote_labels.len());
|
||||
|
||||
for label in remote_labels {
|
||||
if label.exists == 0 {
|
||||
info!(
|
||||
@@ -32,15 +34,14 @@ pub async fn rebuild_cache(
|
||||
match fetch_and_save_full_label(account, label, label.exists, true).await {
|
||||
Ok((inserted, max_history_id)) => {
|
||||
total_inserted += inserted;
|
||||
|
||||
if let Some(history_id) = max_history_id {
|
||||
GmailCheckPoint::new(account.id, label.id, history_id)
|
||||
.save()
|
||||
.await?;
|
||||
history_ids.push(history_id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Account {}: Failed to sync label '{}'. Error: {}. Removing label entry.",
|
||||
"Account {}: Failed to sync label '{}'. Error: {:#?}. Removing label entry.",
|
||||
account.id, &label.name, e
|
||||
);
|
||||
if let Err(del_err) = GmailLabels::delete(label.id).await {
|
||||
@@ -52,6 +53,11 @@ pub async fn rebuild_cache(
|
||||
}
|
||||
}
|
||||
}
|
||||
let max = max_history_id(&history_ids);
|
||||
if let Some(history_id) = max {
|
||||
let checkpoint = GmailCheckPoint::new(account.id, history_id.to_string());
|
||||
checkpoint.save().await?;
|
||||
}
|
||||
let elapsed_time = start_time.elapsed().as_secs();
|
||||
info!(
|
||||
"Rebuild account cache completed: {} envelopes inserted. {} secs elapsed. \
|
||||
@@ -71,6 +77,7 @@ pub async fn rebuild_cache_since_date(
|
||||
let date = date_since.since_gmail_date()?;
|
||||
|
||||
GmailLabels::batch_insert(remote_labels).await?;
|
||||
let mut history_ids = Vec::with_capacity(remote_labels.len());
|
||||
for label in remote_labels {
|
||||
if label.exists == 0 {
|
||||
info!(
|
||||
@@ -83,12 +90,8 @@ pub async fn rebuild_cache_since_date(
|
||||
match fetch_and_save_since_date(account, date.as_str(), label, true).await {
|
||||
Ok((inserted, max_history_id)) => {
|
||||
total_inserted += inserted;
|
||||
// After each label finishes syncing, record its checkpoint individually.
|
||||
// This avoids fetching a large amount of unnecessary history records.
|
||||
if let Some(history_id) = max_history_id {
|
||||
GmailCheckPoint::new(account.id, label.id, history_id)
|
||||
.save()
|
||||
.await?;
|
||||
history_ids.push(history_id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -106,6 +109,11 @@ pub async fn rebuild_cache_since_date(
|
||||
}
|
||||
}
|
||||
|
||||
let max = max_history_id(&history_ids);
|
||||
if let Some(history_id) = max {
|
||||
let checkpoint = GmailCheckPoint::new(account.id, history_id.to_string());
|
||||
checkpoint.save().await?;
|
||||
}
|
||||
let elapsed_time = start_time.elapsed().as_secs();
|
||||
info!(
|
||||
"Rebuild account cache completed: {} envelopes inserted. {} secs elapsed. \
|
||||
@@ -118,7 +126,7 @@ pub async fn rebuild_cache_since_date(
|
||||
pub async fn rebuild_single_label_cache(
|
||||
account: &AccountV2,
|
||||
label: &GmailLabels,
|
||||
) -> RustMailerResult<()> {
|
||||
) -> RustMailerResult<Option<String>> {
|
||||
if label.exists > 0 {
|
||||
match &account.date_since {
|
||||
Some(date_since) => {
|
||||
@@ -129,15 +137,11 @@ pub async fn rebuild_single_label_cache(
|
||||
"Account {}: Label '{}' synced successfully. {} messages inserted.",
|
||||
account.id, label.name, inserted
|
||||
);
|
||||
if let Some(history_id) = max_history_id {
|
||||
GmailCheckPoint::new(account.id, label.id, history_id)
|
||||
.save()
|
||||
.await?;
|
||||
}
|
||||
return Ok(max_history_id);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Account {}: Failed to sync mailbox '{}'. Error: {}. Removing mailbox entry.",
|
||||
"Account {}: Failed to sync mailbox '{}'. Error: {}. Removing label entry.",
|
||||
account.id, &label.name, e
|
||||
);
|
||||
if let Err(del_err) = GmailLabels::delete(label.id).await {
|
||||
@@ -155,15 +159,11 @@ pub async fn rebuild_single_label_cache(
|
||||
"Account {}: Label '{}' synced successfully. {} messages inserted.",
|
||||
account.id, label.name, inserted
|
||||
);
|
||||
if let Some(history_id) = max_history_id {
|
||||
GmailCheckPoint::new(account.id, label.id, history_id)
|
||||
.save()
|
||||
.await?;
|
||||
}
|
||||
return Ok(max_history_id);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Account {}: Failed to sync label '{}'. Error: {}. Removing label entry.",
|
||||
"Account {}: Failed to sync label '{}'. Error: {:#?}. Removing label entry.",
|
||||
account.id, &label.name, e
|
||||
);
|
||||
if let Err(del_err) = GmailLabels::delete(label.id).await {
|
||||
@@ -176,5 +176,5 @@ pub async fn rebuild_single_label_cache(
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -113,6 +113,5 @@ pub async fn retrieve_label_metadata(
|
||||
Err(e) => return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(details)
|
||||
}
|
||||
|
||||
Vendored
+2
-3
@@ -38,7 +38,7 @@ async fn access_token() -> String {
|
||||
grpc_client.set_send_compressed(CompressionEncoding::GZIP);
|
||||
|
||||
let request = GetOAuth2TokensRequest {
|
||||
account_id: 7397694139904449,
|
||||
account_id: 1908057970788951,
|
||||
};
|
||||
|
||||
let mut request = poem_grpc::Request::new(request);
|
||||
@@ -54,14 +54,13 @@ async fn access_token() -> String {
|
||||
async fn test1() {
|
||||
let access_token = access_token().await;
|
||||
let url = "https://gmail.googleapis.com/gmail/v1/users/me/labels/Label_6886728075529239043";
|
||||
let url = "https://gmail.googleapis.com/gmail/v1/users/me/messages?labelIds=INBOX&maxResults=10&pageToken=08792416985640480557";
|
||||
let url =
|
||||
"https://gmail.googleapis.com/gmail/v1/users/me/messages/198e590baf688394?format=metadata";
|
||||
let url =
|
||||
"https://gmail.googleapis.com/gmail/v1/users/me/messages?labelIds=INBOX&maxResults=10";
|
||||
let url = "https://gmail.googleapis.com/gmail/v1/users/me/labels/Label_6886728075529239043";
|
||||
let url = "https://gmail.googleapis.com/gmail/v1/users/me/labels";
|
||||
|
||||
let url = "https://gmail.googleapis.com/gmail/v1/users/me/messages?labelIds=SENT&maxResults=20";
|
||||
let mut builder = reqwest::ClientBuilder::new()
|
||||
.user_agent(rustmailer_version!())
|
||||
.timeout(Duration::from_secs(10))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::modules::{cache::imap::task::IMAP_TASKS, error::RustMailerResult};
|
||||
use crate::modules::{cache::imap::task::SYNC_TASKS, error::RustMailerResult};
|
||||
use std::{sync::LazyLock, time::Duration};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{error, info};
|
||||
@@ -44,10 +44,10 @@ impl SyncController {
|
||||
|
||||
async fn start_syncer(account_id: u64, email: String) -> RustMailerResult<Option<()>> {
|
||||
info!(
|
||||
"IMAP syncer starting for account: {}-{}.",
|
||||
"Account syncer starting for account: {}-{}.",
|
||||
account_id, email
|
||||
);
|
||||
IMAP_TASKS.start_account_task(account_id, email).await;
|
||||
SYNC_TASKS.start_account_sync_task(account_id, email).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ impl EmailClientExecutors {
|
||||
let active_accounts: Vec<AccountV2> = accounts.into_iter().filter(|a| a.enabled).collect();
|
||||
|
||||
if active_accounts.is_empty() {
|
||||
info!("No active accounts found for IMAP initialization.");
|
||||
info!("No active accounts found for account initialization.");
|
||||
return Ok(());
|
||||
}
|
||||
info!(
|
||||
|
||||
@@ -8,7 +8,10 @@ use crate::{
|
||||
id,
|
||||
modules::{
|
||||
account::{entity::AccountKey, v2::AccountV2},
|
||||
cache::imap::mailbox::MailBox,
|
||||
cache::{
|
||||
imap::{mailbox::MailBox, ENVELOPE_MODELS},
|
||||
vendor::gmail::sync::{envelope::GmailEnvelope, flow::max_history_id},
|
||||
},
|
||||
database::META_MODELS,
|
||||
hook::{
|
||||
entity::{EventHooks, HookType, HttpConfig, HttpMethod},
|
||||
@@ -111,3 +114,34 @@ fn test6() {
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&test).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test7() {
|
||||
let database = Builder::new()
|
||||
.create(
|
||||
&ENVELOPE_MODELS,
|
||||
PathBuf::from("D://rustmailer_data//envelope.db"),
|
||||
)
|
||||
.unwrap();
|
||||
//database.compact().unwrap();
|
||||
let r_transaction = database.r_transaction().unwrap();
|
||||
let entities: Vec<GmailEnvelope> = r_transaction
|
||||
.scan()
|
||||
.primary()
|
||||
.unwrap()
|
||||
.all()
|
||||
.unwrap()
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
// println!("{:#?}", entities);
|
||||
|
||||
let history_ids: Vec<String> = entities
|
||||
.into_iter()
|
||||
.filter(|e| e.label_name == "INBOX")
|
||||
.map(|e| e.history_id)
|
||||
.collect();
|
||||
println!("{}", history_ids.len());
|
||||
|
||||
let max_id = max_history_id(&history_ids);
|
||||
println!("{:#?}", max_id);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
use dashmap::DashMap;
|
||||
use http::header::{AUTHORIZATION, CONTENT_TYPE};
|
||||
use http::StatusCode;
|
||||
use tracing::error;
|
||||
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::hook::entity::HttpMethod;
|
||||
@@ -119,7 +121,7 @@ impl HttpClient {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
raise_error!(
|
||||
format!("Request failed for URL {}: {:#?}", url, e),
|
||||
format!("Request failed: {:#?}", e),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
@@ -127,7 +129,7 @@ impl HttpClient {
|
||||
if res.status().is_success() {
|
||||
let json: serde_json::Value = res.json().await.map_err(|e| {
|
||||
raise_error!(
|
||||
format!("Failed to parse response from URL {}: {:#?}", url, e),
|
||||
format!("Failed to parse response: {:#?}", e),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
@@ -136,14 +138,21 @@ impl HttpClient {
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|e| {
|
||||
raise_error!(
|
||||
format!("Failed to read error response from URL {}: {:#?}", url, e),
|
||||
format!("Failed to read error response: {:#?}", e),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
if status.is_client_error() {
|
||||
if matches!(status, StatusCode::NOT_FOUND) || matches!(status, StatusCode::BAD_REQUEST)
|
||||
{
|
||||
error!(
|
||||
status = ?status,
|
||||
url = %url,
|
||||
response = %text,
|
||||
"Gmail API client error"
|
||||
);
|
||||
return Err(raise_error!(
|
||||
format!(
|
||||
"Gmail API returned client error (status {}) for {}: historyId may be invalid or expired. Response: {}",
|
||||
"Gmail API returned client error (status {}) for {}. Response: {}",
|
||||
status, url, text
|
||||
),
|
||||
ErrorCode::GmailApiInvalidHistoryId
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub async fn create_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
executor
|
||||
.create_mailbox(encode_mailbox_name!(mailbox_name).as_str())
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::{encode_mailbox_name, modules::{
|
||||
account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult,
|
||||
}};
|
||||
use crate::{
|
||||
encode_mailbox_name,
|
||||
modules::{
|
||||
account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn delete_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
executor.delete_mailbox(encode_mailbox_name!(mailbox_name).as_str()).await
|
||||
executor
|
||||
.delete_mailbox(encode_mailbox_name!(mailbox_name).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::modules::account::entity::MailerType;
|
||||
use crate::modules::account::v2::AccountV2;
|
||||
use crate::modules::cache::imap::mailbox::{Attribute, AttributeEnum, MailBox};
|
||||
use crate::modules::cache::vendor::gmail::model::labels::{Label, LabelDetail};
|
||||
use crate::modules::cache::vendor::gmail::sync::client::GmailClient;
|
||||
use crate::modules::cache::vendor::gmail::sync::labels::GmailLabels;
|
||||
use crate::modules::context::executors::RUST_MAIL_CONTEXT;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::error::{RustMailerError, RustMailerResult};
|
||||
@@ -15,17 +21,23 @@ pub async fn get_account_mailboxes(
|
||||
account_id: u64,
|
||||
remote: bool,
|
||||
) -> RustMailerResult<Vec<MailBox>> {
|
||||
let account = AccountV2::check_account_active(account_id).await?;
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
let remote = remote || account.minimal_sync();
|
||||
if remote {
|
||||
request_imap_all_mailbox_list(account_id).await
|
||||
} else {
|
||||
MailBox::list_all(account_id).await
|
||||
|
||||
match (&account.mailer_type, remote) {
|
||||
(MailerType::ImapSmtp, true) => request_imap_all_mailbox_list(account_id).await,
|
||||
(MailerType::ImapSmtp, false) => MailBox::list_all(account_id).await,
|
||||
|
||||
(MailerType::GmailApi, true) => request_gmail_label_list(&account).await,
|
||||
(MailerType::GmailApi, false) => {
|
||||
let labels = GmailLabels::list_all(account_id).await?;
|
||||
Ok(labels.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_subscribed_mailboxes(account_id: u64) -> RustMailerResult<Vec<MailBox>> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, true).await?;
|
||||
request_imap_subscribed_mailbox_list(account_id).await
|
||||
}
|
||||
|
||||
@@ -43,6 +55,50 @@ pub async fn request_imap_all_mailbox_list(account_id: u64) -> RustMailerResult<
|
||||
convert_names_to_mailboxes(account_id, names.iter()).await
|
||||
}
|
||||
|
||||
pub async fn request_gmail_label_list(account: &AccountV2) -> RustMailerResult<Vec<MailBox>> {
|
||||
let all_labels = GmailClient::list_labels(account.id, account.use_proxy).await?;
|
||||
let visible_labels: Vec<Label> = all_labels
|
||||
.labels
|
||||
.into_iter()
|
||||
.filter(|label| label.message_list_visibility.as_deref() != Some("hide"))
|
||||
.collect();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
let account = Arc::new(account.clone());
|
||||
for label in visible_labels.into_iter() {
|
||||
let label_id = label.id.clone();
|
||||
let account = account.clone();
|
||||
let task: tokio::task::JoinHandle<Result<LabelDetail, RustMailerError>> =
|
||||
tokio::spawn(async move {
|
||||
GmailClient::get_label(account.id, account.use_proxy, label_id.as_str()).await
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
let mut details = Vec::new();
|
||||
|
||||
for task in tasks {
|
||||
match task.await {
|
||||
Ok(Ok(detail)) => details.push(detail),
|
||||
Ok(Err(err)) => return Err(err),
|
||||
Err(e) => return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)),
|
||||
}
|
||||
}
|
||||
|
||||
let mailboxes: Vec<MailBox> = details
|
||||
.into_iter()
|
||||
.map(|label| {
|
||||
let mut label: GmailLabels = label.into();
|
||||
label.account_id = account.id;
|
||||
label.id = mailbox_id(account.id, &label.label_id);
|
||||
label.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(mailboxes)
|
||||
}
|
||||
|
||||
fn contains_no_select(attributes: &[Attribute]) -> bool {
|
||||
attributes
|
||||
.iter()
|
||||
|
||||
@@ -25,7 +25,7 @@ pub async fn rename_mailbox(
|
||||
account_id: u64,
|
||||
payload: MailboxRenameRequest,
|
||||
) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
executor
|
||||
.rename_mailbox(
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub async fn subscribe_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, true).await?;
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
executor
|
||||
.subscribe_mailbox(encode_mailbox_name!(mailbox_name).as_str())
|
||||
@@ -18,7 +18,7 @@ pub async fn subscribe_mailbox(account_id: u64, mailbox_name: &str) -> RustMaile
|
||||
}
|
||||
|
||||
pub async fn unsubscribe_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, true).await?;
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
executor
|
||||
.unsubscribe_mailbox(encode_mailbox_name!(mailbox_name).as_str())
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct AppendReplyToDraftRequest {
|
||||
|
||||
impl AppendReplyToDraftRequest {
|
||||
pub async fn append_reply_to_draft(&self, account_id: u64) -> RustMailerResult<()> {
|
||||
let account = AccountV2::check_account_active(account_id).await?;
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
let envelope = EmailHandler::get_envelope(&account, &self.mailbox_name, self.uid).await?;
|
||||
let from = Address::new_address(
|
||||
account.name.as_ref().map(|n| Cow::Owned(n.to_string())),
|
||||
|
||||
@@ -59,7 +59,7 @@ pub async fn retrieve_email_attachment(
|
||||
account_id: u64,
|
||||
request: AttachmentRequest,
|
||||
) -> RustMailerResult<cacache::Reader> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
|
||||
if request.attachment.size >= MAX_ATTACHMENT_SIZE {
|
||||
return Err(raise_error!(
|
||||
|
||||
@@ -283,7 +283,7 @@ pub async fn retrieve_email_content(
|
||||
request: MessageContentRequest,
|
||||
skip_cache: bool,
|
||||
) -> RustMailerResult<MessageContent> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
|
||||
let mut plain: Option<PlainText> = None;
|
||||
let mut html: Option<String> = None;
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn copy_mailbox_messages(
|
||||
payload: &MailboxTransferRequest,
|
||||
) -> RustMailerResult<()> {
|
||||
// Ensure the account exists before proceeding
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
|
||||
// Generate a set of UIDs from the payload
|
||||
let uid_set = generate_uid_set(payload.uids.clone());
|
||||
|
||||
@@ -25,7 +25,7 @@ pub async fn move_to_trash_or_delete_messages_directly(
|
||||
account_id: u64,
|
||||
request: &MessageDeleteRequest,
|
||||
) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
let uid_set = generate_uid_set(request.uids.clone());
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ impl FlagAction {
|
||||
}
|
||||
|
||||
pub async fn modify_flags(account_id: u64, request: FlagMessageRequest) -> RustMailerResult<()> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, true).await?;
|
||||
request.validate()?;
|
||||
|
||||
let executor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
|
||||
@@ -35,7 +35,7 @@ pub async fn retrieve_full_email(
|
||||
mailbox: String,
|
||||
uid: u32,
|
||||
) -> RustMailerResult<cacache::Reader> {
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
let meta = get_minimal_meta(account_id, &mailbox, uid).await?;
|
||||
if meta.size > MAX_EMAIL_TOTAL_SIZE {
|
||||
return Err(raise_error!(format!(
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn list_messages_in_mailbox(
|
||||
remote: bool,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
let account = AccountV2::check_account_active(account_id).await?;
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
validate_pagination_params(page, page_size)?;
|
||||
let remote = remote || account.minimal_sync();
|
||||
|
||||
@@ -125,7 +125,7 @@ pub async fn list_threads_in_mailbox(
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
let account = AccountV2::check_account_active(account_id).await?;
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
validate_pagination_params(page, page_size)?;
|
||||
if account.minimal_sync() {
|
||||
return Err(raise_error!(
|
||||
@@ -160,7 +160,7 @@ pub async fn get_thread_messages(
|
||||
mailbox_name: &str,
|
||||
thread_id: u64,
|
||||
) -> RustMailerResult<Vec<EmailEnvelopeV3>> {
|
||||
let account = AccountV2::check_account_active(account_id).await?;
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
if account.minimal_sync() {
|
||||
return Err(raise_error!(
|
||||
format!(
|
||||
|
||||
@@ -14,7 +14,7 @@ pub async fn move_mailbox_messages(
|
||||
payload: &MailboxTransferRequest,
|
||||
) -> RustMailerResult<()> {
|
||||
// Ensure the account exists before proceeding
|
||||
AccountV2::check_account_active(account_id).await?;
|
||||
AccountV2::check_account_active(account_id, false).await?;
|
||||
|
||||
// Generate a set of UIDs from the payload
|
||||
let uid_set = generate_uid_set(payload.uids.clone());
|
||||
|
||||
@@ -437,7 +437,7 @@ impl MessageSearchRequest {
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
let account = AccountV2::check_account_active(account_id).await?;
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
self.search_remote(&account, page, page_size, desc).await
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ impl AccountApi {
|
||||
) -> ApiResult<Json<AccountV2>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
println!("{}", account_id);
|
||||
Ok(Json(AccountV2::get(account_id).await?))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { CalendarIcon, Loader2 } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import useProxyList from '@/hooks/use-proxy';
|
||||
|
||||
|
||||
const relativeDateSchema = z.object({
|
||||
@@ -47,6 +48,7 @@ const accountSchema = () =>
|
||||
email: z.string({ required_error: 'Email is required' }).email({ message: 'Invalid email address' }),
|
||||
enabled: z.boolean(),
|
||||
minimal_sync: z.boolean(),
|
||||
use_proxy: z.number().optional(),
|
||||
date_since: dateSelectionSchema.optional(),
|
||||
incremental_sync_interval_sec: z.number({ invalid_type_error: 'Incremental sync interval must be a number' }).int().min(1, { message: 'Incremental sync interval must be at least 1 second' }),
|
||||
});
|
||||
@@ -64,6 +66,7 @@ export type GmailApiAccount = {
|
||||
value?: number;
|
||||
};
|
||||
};
|
||||
use_proxy?: number,
|
||||
incremental_sync_interval_sec: number;
|
||||
};
|
||||
|
||||
@@ -82,7 +85,8 @@ const defaultValues: GmailApiAccount = {
|
||||
enabled: true,
|
||||
date_since: undefined,
|
||||
incremental_sync_interval_sec: 30,
|
||||
minimal_sync: false
|
||||
minimal_sync: false,
|
||||
use_proxy: undefined
|
||||
};
|
||||
|
||||
|
||||
@@ -94,6 +98,7 @@ const mapCurrentRowToFormValues = (currentRow: AccountEntity): GmailApiAccount =
|
||||
minimal_sync: currentRow.minimal_sync ?? false,
|
||||
date_since: currentRow.date_since ?? undefined,
|
||||
incremental_sync_interval_sec: currentRow.incremental_sync_interval_sec,
|
||||
use_proxy: currentRow.use_proxy
|
||||
};
|
||||
return account;
|
||||
};
|
||||
@@ -110,6 +115,8 @@ export function GmailApiAccountDialog({ currentRow, open, onOpenChange }: Props)
|
||||
: "none"
|
||||
: "none")
|
||||
|
||||
const { proxyOptions } = useProxyList();
|
||||
|
||||
const form = useForm<GmailApiAccount>({
|
||||
mode: "all",
|
||||
defaultValues: isEdit ? mapCurrentRowToFormValues(currentRow) : defaultValues,
|
||||
@@ -166,6 +173,7 @@ export function GmailApiAccountDialog({ currentRow, open, onOpenChange }: Props)
|
||||
date_since: data.date_since,
|
||||
minimal_sync: data.minimal_sync,
|
||||
incremental_sync_interval_sec: data.incremental_sync_interval_sec,
|
||||
use_proxy: data.use_proxy
|
||||
};
|
||||
if (isEdit) {
|
||||
updateMutation.mutate(commonData);
|
||||
@@ -417,6 +425,42 @@ export function GmailApiAccountDialog({ currentRow, open, onOpenChange }: Props)
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='use_proxy'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">Use Proxy(optional):</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
onValueChange={(val) => field.onChange(Number(val))}
|
||||
defaultValue={field.value?.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a proxy" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{proxyOptions && proxyOptions.length > 0 ? (
|
||||
proxyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem disabled value="__none__">No proxy available</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription className='flex-1'>
|
||||
Use a SOCKS5 proxy for Gmail API connections.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface AccountEntity {
|
||||
incremental_sync_interval_sec: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
use_proxy?: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user