feat(account): add MailerType to support Gmail API and other mailers

- Introduced `MailerType` enum in Account model
- Enables selection of different mailer backends (e.g., Gmail API)
This commit is contained in:
rustmailer
2025-08-28 03:20:49 +08:00
parent a9ef330418
commit fd64acc24f
65 changed files with 971 additions and 639 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ use tracing::info;
use crate::{
id,
modules::{
cache::imap::envelope_v2::EmailEnvelopeV2,
cache::imap::v2::EmailEnvelopeV2,
database::{batch_delete_impl, filter_by_secondary_key_impl, manager::DB_MANAGER},
error::{code::ErrorCode, RustMailerResult},
utils::envelope_hash,
+6 -6
View File
@@ -9,13 +9,13 @@ use std::collections::HashSet;
use std::sync::LazyLock;
use tracing::warn;
use crate::modules::account::entity::Account;
use crate::modules::account::v2::AccountV2;
use crate::modules::cache::imap::address::AddressEntity;
use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2;
use crate::modules::cache::imap::flags_to_hash;
use crate::modules::cache::imap::mailbox::EnvelopeFlag;
use crate::modules::cache::imap::minimal::MinimalEnvelope;
use crate::modules::cache::imap::thread::EmailThread;
use crate::modules::cache::imap::v2::EmailEnvelopeV2;
use crate::modules::context::Initialize;
use crate::modules::error::RustMailerResult;
use crate::modules::hook::channel::{Event, EVENT_CHANNEL};
@@ -36,7 +36,7 @@ pub struct EnvelopeFlagsManager;
impl EnvelopeFlagsManager {
pub async fn load_state() -> RustMailerResult<()> {
let all_accounts = Account::list_all().await?;
let all_accounts = AccountV2::list_all().await?;
stream::iter(all_accounts)
.filter(|account| futures::future::ready(account.enabled))
@@ -132,13 +132,13 @@ impl EnvelopeFlagsManager {
}
pub async fn update_envelope_flags(
account: &Account,
account: &AccountV2,
mailbox_id: u64,
data: Vec<(u32, Vec<EnvelopeFlag>)>,
) -> RustMailerResult<()> {
RUSTMAILER_MAIL_FLAG_CHANGE_TOTAL.inc_by(data.len() as u64);
for (uid, flags) in data {
if !account.minimal_sync
if !account.minimal_sync()
&& EventHookTask::event_watched(account.id, EventType::EmailFlagsChanged).await?
{
if let Some(current) = EmailEnvelopeV2::find(account.id, mailbox_id, uid).await? {
@@ -170,7 +170,7 @@ impl EnvelopeFlagsManager {
}
let flags_hash = flags_to_hash(&flags);
if !account.minimal_sync {
if !account.minimal_sync() {
EmailEnvelopeV2::update_flags(account.id, mailbox_id, uid, &flags, flags_hash)
.await?;
}
+1 -1
View File
@@ -11,7 +11,7 @@ use tracing::{error, info};
use crate::{
modules::{
cache::imap::{envelope_v2::EmailEnvelopeV2, manager::EnvelopeFlagsManager},
cache::imap::{v2::EmailEnvelopeV2, manager::EnvelopeFlagsManager},
database::{
batch_delete_impl, batch_insert_impl, delete_impl, filter_by_secondary_key_impl,
manager::DB_MANAGER, update_impl,
+3 -3
View File
@@ -8,8 +8,8 @@ use crate::{
calculate_hash,
modules::{
cache::imap::{
address::AddressEntity, envelope::EmailEnvelope, envelope_v2::EmailEnvelopeV2,
minimal::MinimalEnvelope, thread::EmailThread,
address::AddressEntity, envelope::EmailEnvelope, minimal::MinimalEnvelope,
thread::EmailThread, v2::EmailEnvelopeV2,
},
database::ModelsAdapter,
},
@@ -19,7 +19,6 @@ use mailbox::{EmailFlag, EnvelopeFlag, MailBox};
use native_db::Models;
pub mod address;
pub mod envelope;
pub mod envelope_v2;
pub mod mailbox;
pub mod manager;
pub mod minimal;
@@ -28,6 +27,7 @@ pub mod task;
#[cfg(test)]
mod tests;
pub mod thread;
pub mod v2;
pub static ENVELOPE_MODELS: LazyLock<Models> = LazyLock::new(|| {
let mut adapter = ModelsAdapter::new();
+22 -23
View File
@@ -4,12 +4,10 @@
use crate::{
modules::{
account::{entity::Account, since::DateSince, status::AccountRunningState},
account::{since::DateSince, status::AccountRunningState, v2::AccountV2},
bounce::parser::{extract_bounce_report, BounceReport},
cache::imap::{
diff,
envelope_v2::EmailEnvelopeV2,
find_deleted_mailboxes, find_flag_updates, find_intersecting_mailboxes,
diff, find_deleted_mailboxes, find_flag_updates, find_intersecting_mailboxes,
find_missing_mailboxes, find_missing_remote_uids,
mailbox::{EnvelopeFlag, MailBox},
manager::EnvelopeFlagsManager,
@@ -18,6 +16,7 @@ use crate::{
rebuild::{rebuild_mailbox_cache, rebuild_mailbox_cache_since_date},
sync_type::SyncType,
},
v2::EmailEnvelopeV2,
},
common::AddrVec,
context::executors::RUST_MAIL_CONTEXT,
@@ -138,7 +137,7 @@ pub async fn fetch_and_save_since_date(
}
pub async fn fetch_and_save_full_mailbox(
account: &Account,
account: &AccountV2,
mailbox: &MailBox,
total: u32,
initial: bool,
@@ -148,7 +147,7 @@ pub async fn fetch_and_save_full_mailbox(
let mut inserted_count = 0;
let account_id = account.id;
let minimal_sync = account.minimal_sync;
let minimal_sync = account.minimal_sync();
if initial {
AccountRunningState::set_initial_current_syncing_folder(
@@ -298,7 +297,7 @@ pub fn compress_uid_list(nums: Vec<u32>) -> String {
}
pub async fn compare_and_sync_mailbox(
account: &Account,
account: &AccountV2,
remote_mailboxes: &[MailBox],
local_mailboxes: &[MailBox],
sync_type: &SyncType,
@@ -407,7 +406,7 @@ pub async fn compare_and_sync_mailbox(
}
async fn cleanup_deleted_mailboxes(
account: &Account,
account: &AccountV2,
deleted_mailboxes: &[MailBox],
) -> RustMailerResult<()> {
let start_time = Instant::now();
@@ -424,7 +423,7 @@ async fn cleanup_deleted_mailboxes(
}
/// Sync recent 200 envelopes's flags
async fn sync_recent_envelope_flags(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
uid_next: u32,
@@ -457,7 +456,7 @@ async fn sync_recent_envelope_flags(
//only check new emails and sync
async fn perform_incremental_sync(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
sync_count: usize,
@@ -491,10 +490,10 @@ async fn perform_incremental_sync(
.fetch_uid_list(
max_uid + 1,
&remote_mailbox.encoded_name(),
account.minimal_sync,
account.minimal_sync(),
)
.await?;
let uid_list = parse_fetch_metadata(fetches, !account.minimal_sync)?;
let uid_list = parse_fetch_metadata(fetches, !account.minimal_sync())?;
// This is just a precaution in case the server's behavior deviates from expectations.
let uid_list = uid_list
@@ -522,7 +521,7 @@ async fn perform_incremental_sync(
date_since.since_date()?.as_str(),
remote_mailbox,
false,
account.minimal_sync,
account.minimal_sync(),
)
.await?;
}
@@ -544,7 +543,7 @@ async fn perform_incremental_sync(
}
//
async fn perform_full_sync(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
) -> RustMailerResult<()> {
@@ -666,7 +665,7 @@ async fn diff_uids_and_flags(
}
async fn cleanup_missing_remote_emails(
account: &Account,
account: &AccountV2,
mailbox_id: u64,
mailbox_name: &str,
local_uid_flags_index: &AHashMap<u32, u64>,
@@ -687,7 +686,7 @@ async fn cleanup_missing_remote_emails(
}
pub async fn fetch_and_store_new_envelopes_by_uid_list(
account: &Account,
account: &AccountV2,
local_mailbox_id: u64,
remote: &MailBox,
uid_list: Vec<(u32, u64)>,
@@ -718,7 +717,7 @@ pub async fn fetch_and_store_new_envelopes_by_uid_list(
info!("Account {}: Mailbox '{}' has {} new message UID(s) to fetch metadata. Starting download...", account.id, &remote.name, len);
// Process minimal sync case first
if account.minimal_sync {
if account.minimal_sync() {
let envelopes: Vec<MinimalEnvelope> = uid_list
.clone()
.into_iter()
@@ -768,7 +767,7 @@ pub async fn fetch_and_store_new_envelopes_by_uid_list(
}
async fn process_email_added_events(
account: &Account,
account: &AccountV2,
remote: &MailBox,
fetches: &[Fetch],
) -> RustMailerResult<()> {
@@ -834,13 +833,13 @@ async fn process_email_added_events(
}
async fn handle_minimal_sync_or_metadata_fetch(
account: &Account,
account: &AccountV2,
local_mailbox_id: u64,
remote: &MailBox,
uid_list: Vec<(u32, u64)>,
len: usize,
) -> RustMailerResult<()> {
if account.minimal_sync {
if account.minimal_sync() {
let envelopes: Vec<MinimalEnvelope> = uid_list
.into_iter()
.map(|(uid, flags_hash)| MinimalEnvelope {
@@ -877,7 +876,7 @@ async fn handle_minimal_sync_or_metadata_fetch(
}
async fn process_bounce_reports(
account: &Account,
account: &AccountV2,
remote: &MailBox,
fetches: &[Fetch],
) -> RustMailerResult<()> {
@@ -925,7 +924,7 @@ async fn process_bounce_reports(
}
async fn submit_bounce_event(
account: &Account,
account: &AccountV2,
remote: &MailBox,
uid: u32,
fetch: &Fetch,
@@ -960,7 +959,7 @@ async fn submit_bounce_event(
}
async fn submit_feedback_report_event(
account: &Account,
account: &AccountV2,
remote: &MailBox,
uid: u32,
fetch: &Fetch,
+2 -2
View File
@@ -3,7 +3,7 @@
// Unauthorized copying, modification, or distribution is prohibited.
use crate::modules::{
account::{entity::Account, status::AccountRunningState},
account::{status::AccountRunningState, v2::AccountV2},
cache::imap::{mailbox::MailBox, manager::EnvelopeFlagsManager},
error::RustMailerResult,
hook::{
@@ -29,7 +29,7 @@ pub mod sync_type;
static SYNC_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub async fn execute_account_sync(account: &Account) -> RustMailerResult<()> {
pub async fn execute_account_sync(account: &AccountV2) -> RustMailerResult<()> {
let start_time = Instant::now();
let account_id = account.id;
+8 -16
View File
@@ -3,7 +3,7 @@
// Unauthorized copying, modification, or distribution is prohibited.
use crate::modules::{
account::{entity::Account, since::DateSince},
account::{since::DateSince, v2::AccountV2},
cache::imap::{
mailbox::MailBox,
manager::EnvelopeFlagsManager,
@@ -15,7 +15,7 @@ use std::time::Instant;
use tracing::{error, info, warn};
pub async fn rebuild_cache(
account: &Account,
account: &AccountV2,
remote_mailboxes: &[MailBox],
) -> RustMailerResult<()> {
let start_time = Instant::now();
@@ -62,7 +62,7 @@ pub async fn rebuild_cache(
}
pub async fn rebuild_cache_since_date(
account: &Account,
account: &AccountV2,
remote_mailboxes: &[MailBox],
date_since: &DateSince,
) -> RustMailerResult<()> {
@@ -80,21 +80,13 @@ pub async fn rebuild_cache_since_date(
);
continue;
}
// total_inserted += fetch_and_save_since_date(
// account.id,
// date.as_str(),
// mailbox,
// true,
// account.minimal_sync,
// )
// .await?;
match fetch_and_save_since_date(
account.id,
date.as_str(),
mailbox,
true,
account.minimal_sync,
account.minimal_sync(),
)
.await
{
@@ -126,7 +118,7 @@ pub async fn rebuild_cache_since_date(
}
pub async fn should_rebuild_cache(
account: &Account,
account: &AccountV2,
mailbox_count: usize,
local_envelope_count: usize,
) -> RustMailerResult<bool> {
@@ -147,7 +139,7 @@ pub async fn should_rebuild_cache(
}
pub async fn rebuild_mailbox_cache(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
) -> RustMailerResult<()> {
@@ -171,7 +163,7 @@ pub async fn rebuild_mailbox_cache(
}
pub async fn rebuild_mailbox_cache_since_date(
account: &Account,
account: &AccountV2,
local_mailbox_id: u64,
date_since: &DateSince,
remote: &MailBox,
@@ -191,7 +183,7 @@ pub async fn rebuild_mailbox_cache_since_date(
date_since.since_date()?.as_str(),
remote,
false,
account.minimal_sync,
account.minimal_sync(),
)
.await?;
info!(
+8 -8
View File
@@ -7,7 +7,7 @@ use std::collections::BTreeSet;
use crate::{
decode_mailbox_name,
modules::{
account::entity::Account,
account::v2::AccountV2,
cache::imap::mailbox::{AttributeEnum, MailBox},
context::executors::RUST_MAIL_CONTEXT,
error::{code::ErrorCode, RustMailerResult},
@@ -26,7 +26,7 @@ use crate::{
use async_imap::types::Name;
use tracing::{info, warn};
pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox>> {
pub async fn get_sync_folders(account: &AccountV2) -> RustMailerResult<Vec<MailBox>> {
let executor = RUST_MAIL_CONTEXT.imap(account.id).await?;
let names = executor.list_all_mailboxes().await?;
if names.is_empty() {
@@ -45,7 +45,7 @@ pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox
mailboxes.iter().map(|(m, _)| m.name.clone()).collect(),
)
.await?;
let account = Account::get(account.id).await?;
let account = AccountV2::get(account.id).await?;
let subscribed = &account.sync_folders;
let is_noselect = |mailbox: &MailBox| {
mailbox
@@ -83,7 +83,7 @@ pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox
.iter()
.map(|n| decode_mailbox_name!(n.name().to_string()))
.collect();
Account::update_sync_folders(account.id, sync_folders).await?;
AccountV2::update_sync_folders(account.id, sync_folders).await?;
} else {
warn!(
"Account {}: No subscribed mailboxes found. This is unexpected — IMAP server should at least provide INBOX.",
@@ -99,12 +99,12 @@ pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox
}
pub async fn detect_mailbox_changes(
account: &Account,
account: &AccountV2,
all_names: BTreeSet<String>,
) -> RustMailerResult<()> {
if account.known_folders.is_empty() {
// First time sync: just save without comparing
Account::update_known_folders(account.id, all_names).await?;
AccountV2::update_known_folders(account.id, all_names).await?;
return Ok(());
}
@@ -136,7 +136,7 @@ pub async fn detect_mailbox_changes(
// Note: When all subscribed folders are deleted (remaining_sync_folders empty),
// the system's default behavior is to automatically fall back to syncing
// only the default folders (INBOX and Sent) in subsequent operations
Account::update_sync_folders(account.id, remaining_sync_folders).await?;
AccountV2::update_sync_folders(account.id, remaining_sync_folders).await?;
}
info!(
@@ -187,7 +187,7 @@ pub async fn detect_mailbox_changes(
// Update known folders only if there were changes
if has_changes {
Account::update_known_folders(account.id, all_names).await?;
AccountV2::update_known_folders(account.id, all_names).await?;
}
Ok(())
}
+8 -3
View File
@@ -4,12 +4,15 @@
use crate::{
modules::{
account::{entity::Account, status::AccountRunningState},
account::{status::AccountRunningState, v2::AccountV2},
error::RustMailerResult,
},
utc_now,
};
/// Default interval (in minutes) for full synchronization.
pub const DEFAULT_FULL_SYNC_INTERVAL_MIN: i64 = 30;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SyncType {
/// Full synchronization, typically used for the first sync or after major changes.
@@ -20,14 +23,16 @@ pub enum SyncType {
SkipSync,
}
pub async fn determine_sync_type(account: &Account) -> RustMailerResult<SyncType> {
pub async fn determine_sync_type(account: &AccountV2) -> RustMailerResult<SyncType> {
Ok(match AccountRunningState::get(account.id).await? {
Some(info) => {
let now = utc_now!();
if is_time_for_full_sync(
now,
info.last_full_sync_start,
account.full_sync_interval_min,
account
.full_sync_interval_min
.unwrap_or(DEFAULT_FULL_SYNC_INTERVAL_MIN),
) {
AccountRunningState::set_full_sync_start(account.id).await?;
SyncType::FullSync
+11 -3
View File
@@ -7,7 +7,7 @@ use crate::modules::cache::imap::sync::execute_account_sync;
use crate::modules::oauth2::token::OAuth2AccessToken;
use crate::modules::scheduler::periodic::TaskHandle;
use crate::modules::{
account::{dispatcher::STATUS_DISPATCHER, entity::Account},
account::{dispatcher::STATUS_DISPATCHER, v2::AccountV2},
error::RustMailerResult,
scheduler::periodic::PeriodicTask,
};
@@ -40,7 +40,7 @@ impl AccountSyncTask {
let task = move |param: Option<u64>| {
let account_id = param.unwrap();
Box::pin(async move {
let account = Account::get(account_id).await.ok();
let account = AccountV2::get(account_id).await.ok();
match account {
Some(account) => {
if !account.enabled {
@@ -54,7 +54,15 @@ impl AccountSyncTask {
);
}
} else {
if let AuthType::OAuth2 = account.imap.auth.auth_type {
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);
+1 -1
View File
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::{
modules::{
cache::imap::envelope_v2::EmailEnvelopeV2,
cache::imap::v2::EmailEnvelopeV2,
database::{
batch_delete_impl, delete_impl, manager::DB_MANAGER, paginate_secondary_scan_impl,
},