From fd64acc24fa77f090fa6f703a5386dcd308efe06 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Thu, 28 Aug 2025 03:20:49 +0800 Subject: [PATCH] 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) --- protos/rustmailer.proto | 26 +- src/modules/account/entity.rs | 342 +------------ src/modules/account/mod.rs | 1 + src/modules/account/payload.rs | 42 +- src/modules/account/v2.rs | 474 ++++++++++++++++++ src/modules/cache/imap/address.rs | 2 +- src/modules/cache/imap/manager.rs | 12 +- src/modules/cache/imap/minimal.rs | 2 +- src/modules/cache/imap/mod.rs | 6 +- src/modules/cache/imap/sync/flow.rs | 45 +- src/modules/cache/imap/sync/mod.rs | 4 +- src/modules/cache/imap/sync/rebuild.rs | 24 +- src/modules/cache/imap/sync/sync_folders.rs | 16 +- src/modules/cache/imap/sync/sync_type.rs | 11 +- src/modules/cache/imap/task.rs | 14 +- src/modules/cache/imap/thread.rs | 2 +- .../cache/imap/{envelope_v2.rs => v2.rs} | 0 src/modules/context/executors.rs | 6 +- src/modules/database/manager.rs | 16 +- src/modules/database/mod.rs | 2 + src/modules/database/tests.rs | 12 +- src/modules/envelope/extractor.rs | 2 +- src/modules/grpc/service/account/from.rs | 67 ++- src/modules/grpc/service/account/mod.rs | 2 +- src/modules/grpc/service/message/from.rs | 2 +- src/modules/hook/entity.rs | 8 +- src/modules/imap/capabilities.rs | 3 +- src/modules/imap/manager.rs | 49 +- src/modules/license/mod.rs | 4 +- src/modules/mailbox/create.rs | 4 +- src/modules/mailbox/delete.rs | 4 +- src/modules/mailbox/list.rs | 8 +- src/modules/mailbox/rename.rs | 16 +- src/modules/mailbox/subscribe.rs | 6 +- src/modules/message/append.rs | 4 +- src/modules/message/attachment.rs | 4 +- src/modules/message/content.rs | 4 +- src/modules/message/copy.rs | 4 +- src/modules/message/delete.rs | 4 +- src/modules/message/flag.rs | 4 +- src/modules/message/full.rs | 4 +- src/modules/message/list.rs | 18 +- src/modules/message/mv.rs | 4 +- src/modules/message/search/payload.rs | 8 +- src/modules/overview/mod.rs | 4 +- src/modules/rest/api/account.rs | 24 +- src/modules/rest/api/message.rs | 2 +- src/modules/smtp/composer/mod.rs | 4 +- src/modules/smtp/manager.rs | 36 +- src/modules/smtp/request/forward.rs | 10 +- src/modules/smtp/request/mod.rs | 18 +- src/modules/smtp/request/new.rs | 8 +- src/modules/smtp/request/reply.rs | 10 +- src/modules/smtp/request/task.rs | 8 +- src/modules/smtp/template/entity.rs | 10 +- src/modules/smtp/template/send.rs | 4 +- src/modules/token/mod.rs | 6 +- src/modules/token/payload.rs | 6 +- src/modules/vendor/mod.rs | 9 + src/modules/vendor/tests.rs | 44 ++ .../accounts/components/account-detail.tsx | 42 +- .../accounts/components/action-dialog.tsx | 27 +- .../accounts/components/oauth2-action.tsx | 12 +- web/src/features/accounts/data/schema.ts | 24 +- web/src/features/accounts/index.tsx | 10 +- 65 files changed, 971 insertions(+), 639 deletions(-) create mode 100644 src/modules/account/v2.rs rename src/modules/cache/imap/{envelope_v2.rs => v2.rs} (100%) create mode 100644 src/modules/vendor/mod.rs create mode 100644 src/modules/vendor/tests.rs diff --git a/protos/rustmailer.proto b/protos/rustmailer.proto index c30fd5b..70038ac 100644 --- a/protos/rustmailer.proto +++ b/protos/rustmailer.proto @@ -104,9 +104,9 @@ message Account { // The unique identifier of the account. uint64 id = 1; // The IMAP server configuration for the account. - ImapConfig imap = 2; + optional ImapConfig imap = 2; // The SMTP server configuration for the account. - SmtpConfig smtp = 3; + optional SmtpConfig smtp = 3; // Indicates whether the account is enabled for synchronization. bool enabled = 4; // The email address of the account. @@ -114,7 +114,7 @@ message Account { // Optional: A display name for the account. optional string name = 6; // If true, only minimal metadata will be synced initially (e.g., no full message bodies). - bool minimal_sync = 7; + optional bool minimal_sync = 7; // A list of capabilities supported by the account (e.g., "IMAP4REV1", "AUTH=PLAIN"). repeated string capabilities = 8; // Optional: Indicates if the account is capable of Delivery Status Notifications (DSN). @@ -126,13 +126,15 @@ message Account { // A list of known folders (e.g., "INBOX", "Sent Items"). repeated string known_folders = 12; // The interval (in minutes) for a full synchronization pass. - int64 full_sync_interval_min = 13; + optional int64 full_sync_interval_min = 13; // The interval (in seconds) for incremental synchronization updates. int64 incremental_sync_interval_sec = 14; // The timestamp when the account was created. int64 created_at = 15; // The timestamp when the account was last updated. int64 updated_at = 16; + // Method used to access and manage emails. + MailerType mailer_type = 17; } // PagedAccount represents a paginated list of Account messages. @@ -156,19 +158,21 @@ message AccountCreateRequest { // Optional: A display name for the new account. optional string name = 2; // The IMAP configuration for the new account. - ImapConfig imap = 3; + optional ImapConfig imap = 3; // The SMTP configuration for the new account. - SmtpConfig smtp = 4; + optional SmtpConfig smtp = 4; // Indicates whether the new account should be enabled immediately. bool enabled = 5; // Optional: The date from which to start syncing emails for this account. optional DateSince date_since = 6; // If true, only minimal metadata will be synced initially for the new account. - bool minimal_sync = 7; + optional bool minimal_sync = 7; // Optional: The interval (in minutes) for a full synchronization pass. optional int64 full_sync_interval_min = 8; // Optional: The interval (in seconds) for incremental synchronization updates. optional int64 incremental_sync_interval_sec = 9; + // Method used to access and manage emails. + MailerType mailer_type = 10; } // AccountUpdateRequest defines the parameters for updating an existing email account. @@ -261,6 +265,14 @@ message ListMinimalAccountsResponse { repeated MinimalAccount accounts = 1; } +// Represents the method used to access/manage emails. +enum MailerType { + // Default value: use IMAP/SMTP protocol + IMAP_SMTP = 0; + // Use Gmail API + GMAIL_API = 1; +} + // AccountService provides APIs for managing email accounts. service AccountService { // Retrieves a specific email account by its ID. diff --git a/src/modules/account/entity.rs b/src/modules/account/entity.rs index 8ffe71a..5801412 100644 --- a/src/modules/account/entity.rs +++ b/src/modules/account/entity.rs @@ -4,45 +4,13 @@ use std::collections::BTreeSet; -use crate::encrypt; -use crate::id; -use crate::modules::account::payload::AccountCreateRequest; -use crate::modules::account::payload::AccountUpdateRequest; -use crate::modules::account::payload::MinimalAccount; -use crate::modules::account::since::DateSince; -use crate::modules::cache::imap::mailbox::MailBox; -use crate::modules::cache::imap::manager::EnvelopeFlagsManager; -use crate::modules::cache::imap::task::IMAP_TASKS; -use crate::modules::context::controller::SYNC_CONTROLLER; -use crate::modules::context::executors::RUST_MAIL_CONTEXT; -use crate::modules::database::count_by_unique_secondary_key_impl; -use crate::modules::database::delete_impl; -use crate::modules::database::manager::DB_MANAGER; -use crate::modules::database::{ - paginate_query_primary_scan_all_impl, secondary_find_impl, update_impl, -}; -use crate::modules::error::code::ErrorCode; -use crate::modules::hook::entity::EventHooks; -use crate::modules::license::License; -use crate::modules::oauth2::token::OAuth2AccessToken; -use crate::modules::rest::response::DataPage; -use crate::modules::smtp::template::entity::EmailTemplate; -use crate::modules::token::AccessToken; -use crate::raise_error; -use crate::{ - modules::database::{insert_impl, list_all_impl}, - modules::error::RustMailerResult, - utc_now, -}; +use crate::modules::error::RustMailerResult; +use crate::{encrypt, modules::account::since::DateSince}; use native_db::*; use native_model::{native_model, Model}; use poem_openapi::{Enum, Object}; use serde::{Deserialize, Serialize}; -use tracing::error; -use tracing::info; - -use super::status::AccountRunningState; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] #[native_model(id = 5, version = 1)] @@ -109,302 +77,6 @@ impl Account { fn pk(&self) -> String { format!("{}_{}", self.created_at, self.id) } - - pub fn create(request: AccountCreateRequest) -> RustMailerResult { - Ok(Account { - id: id!(64), - email: request.email, - name: request.name, - imap: request.imap.try_encrypt_password()?, - smtp: request.smtp.try_encrypt_password()?, - enabled: request.enabled, - minimal_sync: request.minimal_sync, - capabilities: Vec::new(), - //status: AccountStatus::Registered, - //error_reason: None, - date_since: request.date_since, - dsn_capable: None, - sync_folders: vec![], - known_folders: BTreeSet::new(), - full_sync_interval_min: request.full_sync_interval_min.unwrap_or(30), - incremental_sync_interval_sec: request.incremental_sync_interval_sec.unwrap_or(60), - created_at: utc_now!(), - updated_at: utc_now!(), - }) - } - - pub async fn check_account_active(account_id: u64) -> RustMailerResult { - let account_entity = - secondary_find_impl::(DB_MANAGER.meta_db(), AccountKey::id, account_id) - .await?; - match account_entity { - Some(entity) if entity.enabled => Ok(entity), - Some(_) => Err(raise_error!( - format!("Account id='{account_id}' is disabled"), - ErrorCode::AccountDisabled - )), - None => Err(raise_error!( - format!("Account id='{account_id}' not found"), - ErrorCode::ResourceNotFound - )), - } - } - - /// Fetches an `AccountEntity` by its `id`. - pub async fn get(account_id: u64) -> RustMailerResult { - let result = Self::find(account_id).await?.ok_or_else(|| { - raise_error!( - format!("Account with ID '{account_id}' not found"), - ErrorCode::ResourceNotFound - ) - })?; - Ok(result) - } - - pub async fn find(account_id: u64) -> RustMailerResult> { - secondary_find_impl::(DB_MANAGER.meta_db(), AccountKey::id, account_id).await - } - - /// Saves the current `AccountEntity` by persisting it to storage. - pub async fn save(&self) -> RustMailerResult<()> { - insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await - } - - pub async fn create_account(request: AccountCreateRequest) -> RustMailerResult { - // Validate license limits before creating entity - if let Some(license) = License::get_current_license().await? { - let current_count = Account::count().await?; - if let Some(max_accounts) = license.max_accounts { - if current_count >= max_accounts as usize { - return Err(raise_error!( - "Maximum account limit reached".into(), - ErrorCode::LicenseAccountLimitReached - )); - } - } - } - let entity = request.create_entity()?; - entity.save().await?; - SYNC_CONTROLLER - .trigger_start(entity.id, entity.email.clone()) - .await; - Ok(entity) - } - - pub async fn update( - account_id: u64, - request: AccountUpdateRequest, - validate: bool, - ) -> RustMailerResult<()> { - if validate { - request.validate_update_request()?; - } - update_impl(DB_MANAGER.meta_db(), move |rw| { - rw.get().secondary::(AccountKey::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?.ok_or_else(|| raise_error!(format!( - "Attempted to edit the account's base information, but the corresponding account metadata was not found. account_id={}", - account_id - ), ErrorCode::ResourceNotFound)) - }, |current|{ - Self::apply_update_fields(current, request) - }).await?; - - Ok(()) - } - - pub async fn delete(account_id: u64) -> RustMailerResult<()> { - let request = AccountUpdateRequest { - enabled: Some(false), - ..Default::default() - }; - Self::update(account_id, request, false).await?; - IMAP_TASKS.stop(account_id).await?; - tokio::spawn(async move { - if let Err(e) = Self::cleanup_account_resources_sequential(account_id).await { - error!("Account cleanup failed for {}: {:?}", account_id, e); - } - }); - Ok(()) - } - - async fn delete_account(account_id: u64) -> RustMailerResult<()> { - delete_impl(DB_MANAGER.meta_db(), move|rw|{ - rw.get().secondary::(AccountKey::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? - .ok_or_else(||raise_error!(format!("The account entity with id={account_id} that you want to delete was not found."), ErrorCode::ResourceNotFound)) - }).await - } - - async fn cleanup_account_resources_sequential(account_id: u64) -> RustMailerResult<()> { - let account = Self::get(account_id).await?; - EmailTemplate::remove_account_templates(account_id).await?; - OAuth2AccessToken::try_delete(account_id).await?; - EventHooks::try_delete(account_id).await?; - AccessToken::cleanup_account(account_id).await?; - MailBox::clean(account_id).await?; - AccountRunningState::delete(account_id).await?; - //INDEX_MANAGER.clean(&account).await?; - EnvelopeFlagsManager::clean_account(account.id).await?; - RUST_MAIL_CONTEXT.clean_account(account_id).await?; - Self::delete_account(account_id).await?; - info!("Sequential cleanup completed for account: {}", account_id); - Ok(()) - } - - pub async fn update_sync_folders( - account_id: u64, - sync_folders: Vec, - ) -> RustMailerResult<()> { - update_impl(DB_MANAGER.meta_db(), move |rw| { - rw.get().secondary::(AccountKey::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? - .ok_or_else(|| raise_error!(format!("When trying to update account sync_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) - }, |current|{ - let mut updated = current.clone(); - updated.sync_folders = sync_folders; - Ok(updated) - }).await?; - Ok(()) - } - - pub async fn update_known_folders( - account_id: u64, - known_folders: BTreeSet, - ) -> RustMailerResult<()> { - update_impl(DB_MANAGER.meta_db(), move |rw| { - rw.get().secondary::(AccountKey::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? - .ok_or_else(|| raise_error!(format!("When trying to update account known_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) - }, |current|{ - let mut updated = current.clone(); - updated.known_folders = known_folders; - Ok(updated) - }).await?; - Ok(()) - } - - #[cfg(not(test))] - pub async fn update_capabilities( - account_id: u64, - capabilities: Vec, - ) -> RustMailerResult<()> { - update_impl(DB_MANAGER.meta_db(), move |rw| { - rw.get().secondary::(AccountKey::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? - .ok_or_else(|| raise_error!(format!("When trying to update account capabilities, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) - }, |current|{ - let mut updated = current.clone(); - updated.capabilities = capabilities; - Ok(updated) - }).await?; - Ok(()) - } - - pub async fn update_dsn_capable(account_id: u64, dsn: bool) -> RustMailerResult<()> { - update_impl( - DB_MANAGER.meta_db(), - move |rw| { - rw.get() - .secondary::(AccountKey::id, account_id) - .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? - .ok_or_else(|| { - raise_error!(format!( - "When trying to update account dsn capabilities, the corresponding record was not found. account_id={}", - account_id - ), ErrorCode::ResourceNotFound) - }) - }, - move |current| { - let mut updated = current.clone(); - updated.dsn_capable = Some(dsn); - Ok(updated) - }, - ) - .await?; - Ok(()) - } - - /// Retrieves a list of all `AccountEntity` instances. - pub async fn list_all() -> RustMailerResult> { - list_all_impl(DB_MANAGER.meta_db()).await - } - - pub async fn minimal_list() -> RustMailerResult> { - let result = list_all_impl(DB_MANAGER.meta_db()) - .await? - .into_iter() - .filter(|a: &Account| a.enabled) - .map(|account: Account| MinimalAccount { - id: account.id, - email: account.email.clone(), - }) - .collect::>(); - Ok(result) - } - - pub async fn count() -> RustMailerResult { - count_by_unique_secondary_key_impl::(DB_MANAGER.meta_db(), AccountKey::id).await - } - - pub async fn paginate_list( - page: Option, - page_size: Option, - desc: Option, - ) -> RustMailerResult> { - paginate_query_primary_scan_all_impl(DB_MANAGER.meta_db(), page, page_size, desc) - .await - .map(DataPage::from) - } - - // This method applies the updates from the request to the old account entity - fn apply_update_fields( - old: &Account, - request: AccountUpdateRequest, - ) -> RustMailerResult { - let mut new = old.clone(); - - if let Some(date_since) = request.date_since { - new.date_since = Some(date_since); - } - - if let Some(name) = &request.name { - new.name = Some(name.clone()); - } - - if let Some(imap) = &request.imap { - new.imap.host = imap.host.clone(); - new.imap.port = imap.port.clone(); - new.imap.encryption = imap.encryption.clone(); - new.imap.auth.auth_type = imap.auth.auth_type.clone(); - if let Some(password) = &imap.auth.password { - let encrypted_password = encrypt!(password)?; - new.imap.auth.password = Some(encrypted_password); - } - new.imap.use_proxy = imap.use_proxy; - } - - if let Some(smtp) = &request.smtp { - new.smtp.host = smtp.host.clone(); - new.smtp.port = smtp.port.clone(); - new.smtp.encryption = smtp.encryption.clone(); - new.smtp.auth.auth_type = smtp.auth.auth_type.clone(); - if let Some(password) = &smtp.auth.password { - let encrypted_password = encrypt!(password)?; - new.smtp.auth.password = Some(encrypted_password); - } - new.smtp.use_proxy = smtp.use_proxy; - } - - if let Some(mailboxes) = request.sync_folders { - new.sync_folders = mailboxes; - } - if let Some(full_sync_interval_min) = &request.full_sync_interval_min { - new.full_sync_interval_min = *full_sync_interval_min; - } - if let Some(incremental_sync_interval_sec) = &request.incremental_sync_interval_sec { - new.incremental_sync_interval_sec = *incremental_sync_interval_sec; - } - if let Some(enabled) = request.enabled { - new.enabled = enabled; - } - new.updated_at = utc_now!(); - Ok(new) - } } #[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Object)] @@ -532,3 +204,13 @@ impl From for Encryption { } } } + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Enum)] +pub enum MailerType { + /// Use IMAP/SMTP protocol + #[default] + ImapSmtp, + + /// Use Gmail API + GmailApi, +} diff --git a/src/modules/account/mod.rs b/src/modules/account/mod.rs index 7bf729a..5048c48 100644 --- a/src/modules/account/mod.rs +++ b/src/modules/account/mod.rs @@ -7,3 +7,4 @@ pub mod entity; pub mod payload; pub mod since; pub mod status; +pub mod v2; diff --git a/src/modules/account/payload.rs b/src/modules/account/payload.rs index a2e7838..91c3c51 100644 --- a/src/modules/account/payload.rs +++ b/src/modules/account/payload.rs @@ -4,8 +4,9 @@ use std::collections::BTreeSet; -use crate::modules::account::entity::{Account, ImapConfig, SmtpConfig}; +use crate::modules::account::entity::{ImapConfig, MailerType, SmtpConfig}; use crate::modules::account::since::DateSince; +use crate::modules::account::v2::AccountV2; use crate::modules::error::code::ErrorCode; use crate::modules::error::RustMailerResult; use crate::modules::token::AccountInfo; @@ -24,15 +25,17 @@ pub struct AccountCreateRequest { /// Display name for the account (optional) pub name: Option, /// IMAP server configuration - pub imap: ImapConfig, + pub imap: Option, /// SMTP server configuration - pub smtp: SmtpConfig, + pub smtp: Option, /// Represents the account activation status. /// /// If this value is `false`, all account-related resources will be unavailable /// and any attempts to access them should return an error indicating the account /// is inactive. pub enabled: bool, + /// Method used to access and manage emails. + pub mailer_type: MailerType, /// Controls initial synchronization time range /// /// When dealing with large mailboxes, this restricts scanning to: @@ -50,7 +53,7 @@ pub struct AccountCreateRequest { /// Recommended for: /// - Extremely resource-constrained environments /// - Accounts where only new message notification is needed - pub minimal_sync: bool, + pub minimal_sync: Option, /// Full sync interval (minutes), default 30m #[oai(validator(minimum(value = "10"), maximum(value = "10080")))] pub full_sync_interval_min: Option, @@ -60,13 +63,38 @@ pub struct AccountCreateRequest { } impl AccountCreateRequest { - pub fn create_entity(self) -> RustMailerResult { + pub fn create_entity(self) -> RustMailerResult { if let Some(date_since) = self.date_since.as_ref() { date_since.validate()?; } + if matches!(self.mailer_type, MailerType::ImapSmtp) { + if self.imap.is_none() || self.smtp.is_none() { + return Err(raise_error!( + "Invalid input: Both 'imap' and 'smtp' must be provided.".into(), + ErrorCode::InvalidParameter + )); + } + Self::validate_request( + &self.imap.clone().unwrap(), + &self.smtp.clone().unwrap(), + &self.email, + )?; - Self::validate_request(&self.imap, &self.smtp, &self.email)?; - Ok(Account::create(self)?) + if self.full_sync_interval_min.is_none() { + return Err(raise_error!( + "Invalid input: 'full_sync_interval_min' must be provided.".into(), + ErrorCode::InvalidParameter + )); + } + + if self.incremental_sync_interval_sec.is_none() { + return Err(raise_error!( + "Invalid input: 'incremental_sync_interval_sec' must be provided.".into(), + ErrorCode::InvalidParameter + )); + } + } + Ok(AccountV2::create(self)?) } fn validate_request(imap: &ImapConfig, smtp: &SmtpConfig, email: &str) -> RustMailerResult<()> { diff --git a/src/modules/account/v2.rs b/src/modules/account/v2.rs new file mode 100644 index 0000000..55aaa34 --- /dev/null +++ b/src/modules/account/v2.rs @@ -0,0 +1,474 @@ +// Copyright © 2025 rustmailer.com +// Licensed under RustMailer License Agreement v1.0 +// Unauthorized copying, modification, or distribution is prohibited. + +use native_db::*; +use native_model::{native_model, Model}; + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; +use tracing::{error, info}; + +use crate::{ + encrypt, + modules::{ + account::{ + entity::{Account, ImapConfig, MailerType, SmtpConfig}, + since::DateSince, + status::AccountRunningState, + }, + database::{insert_impl, list_all_impl}, + error::RustMailerResult, + }, + utc_now, +}; + +use crate::id; +use crate::modules::account::payload::AccountCreateRequest; +use crate::modules::account::payload::AccountUpdateRequest; +use crate::modules::account::payload::MinimalAccount; +use crate::modules::cache::imap::mailbox::MailBox; +use crate::modules::cache::imap::manager::EnvelopeFlagsManager; +use crate::modules::cache::imap::task::IMAP_TASKS; +use crate::modules::context::controller::SYNC_CONTROLLER; +use crate::modules::context::executors::RUST_MAIL_CONTEXT; +use crate::modules::database::count_by_unique_secondary_key_impl; +use crate::modules::database::delete_impl; +use crate::modules::database::manager::DB_MANAGER; +use crate::modules::database::{ + paginate_query_primary_scan_all_impl, secondary_find_impl, update_impl, +}; +use crate::modules::error::code::ErrorCode; +use crate::modules::hook::entity::EventHooks; +use crate::modules::license::License; +use crate::modules::oauth2::token::OAuth2AccessToken; +use crate::modules::rest::response::DataPage; +use crate::modules::smtp::template::entity::EmailTemplate; +use crate::modules::token::AccessToken; +use crate::raise_error; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +#[native_model(id = 5, version = 2, from = Account)] +#[native_db(primary_key(pk -> String))] +pub struct AccountV2 { + /// Unique account identifier + #[secondary_key(unique)] + pub id: u64, + /// IMAP server configuration + pub imap: Option, + /// SMTP server configuration + pub smtp: Option, + /// Represents the account activation status. + /// + /// If this value is `false`, all account-related resources will be unavailable + /// and any attempts to access them should return an error indicating the account + /// is inactive. + pub enabled: bool, + /// Method used to access and manage emails. + pub mailer_type: MailerType, + /// Email address associated with this account + #[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))] + pub email: String, + /// Display name for the account (optional) + pub name: Option, + /// Minimal sync mode flag + /// + /// When enabled (`true`), only the most essential metadata will be synchronized: + /// Recommended for: + /// - Extremely resource-constrained environments + /// - Accounts where only new message notification is needed + pub minimal_sync: Option, + /// IMAP Server-supported capability flags + pub capabilities: Option>, + /// DSN (Delivery Status Notification) support flag + pub dsn_capable: Option, + /// Controls initial synchronization time range + /// + /// When dealing with large mailboxes, this restricts scanning to: + /// - Messages after specified starting point + /// - Or within sliding window + /// + /// ### Use Cases + /// - Event-driven systems (only sync recent actionable emails) + /// - First-time sync optimization for large accounts + /// - Reducing server load during resyncs + pub date_since: Option, + /// Configuration for selective folder synchronization + /// + /// Defaults to standard folders (`INBOX`, `Sent`) if empty. + /// Modified folders will be automatically synced on next update. + pub sync_folders: Vec, + /// Full sync interval (minutes), default 30m + pub full_sync_interval_min: Option, + /// Incremental sync interval (seconds), default 60s + pub incremental_sync_interval_sec: i64, + /// Tracks known mail folders and detects changes (creations/deletions) + pub known_folders: BTreeSet, + /// Creation timestamp (UNIX epoch milliseconds) + pub created_at: i64, + /// Last update timestamp (UNIX epoch milliseconds) + pub updated_at: i64, +} + +impl AccountV2 { + fn pk(&self) -> String { + format!("{}_{}", self.created_at, self.id) + } + + pub fn minimal_sync(&self) -> bool { + self.minimal_sync.unwrap_or(false) + } + + pub fn create(request: AccountCreateRequest) -> RustMailerResult { + Ok(Self { + id: id!(64), + email: request.email, + name: request.name, + imap: request + .imap + .map(|imap| imap.try_encrypt_password()) + .transpose()?, + smtp: request + .smtp + .map(|smtp| smtp.try_encrypt_password()) + .transpose()?, + enabled: request.enabled, + mailer_type: request.mailer_type, + minimal_sync: request.minimal_sync, + capabilities: None, + date_since: request.date_since, + dsn_capable: None, + sync_folders: vec![], + known_folders: BTreeSet::new(), + full_sync_interval_min: request.full_sync_interval_min, + incremental_sync_interval_sec: request.incremental_sync_interval_sec.unwrap_or(60), + created_at: utc_now!(), + updated_at: utc_now!(), + }) + } + + pub async fn check_account_active(account_id: u64) -> RustMailerResult { + let account_entity = + secondary_find_impl::(DB_MANAGER.meta_db(), AccountV2Key::id, account_id) + .await?; + match account_entity { + Some(entity) if entity.enabled => Ok(entity), + Some(_) => Err(raise_error!( + format!("Account id='{account_id}' is disabled"), + ErrorCode::AccountDisabled + )), + None => Err(raise_error!( + format!("Account id='{account_id}' not found"), + ErrorCode::ResourceNotFound + )), + } + } + + /// Fetches an `AccountEntity` by its `id`. + pub async fn get(account_id: u64) -> RustMailerResult { + let result = Self::find(account_id).await?.ok_or_else(|| { + raise_error!( + format!("Account with ID '{account_id}' not found"), + ErrorCode::ResourceNotFound + ) + })?; + Ok(result) + } + + pub async fn find(account_id: u64) -> RustMailerResult> { + secondary_find_impl::(DB_MANAGER.meta_db(), AccountV2Key::id, account_id).await + } + + /// Saves the current `AccountEntity` by persisting it to storage. + pub async fn save(&self) -> RustMailerResult<()> { + insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await + } + + pub async fn create_account(request: AccountCreateRequest) -> RustMailerResult { + // Validate license limits before creating entity + if let Some(license) = License::get_current_license().await? { + let current_count = AccountV2::count().await?; + if let Some(max_accounts) = license.max_accounts { + if current_count >= max_accounts as usize { + return Err(raise_error!( + "Maximum account limit reached".into(), + ErrorCode::LicenseAccountLimitReached + )); + } + } + } + let entity = request.create_entity()?; + entity.save().await?; + SYNC_CONTROLLER + .trigger_start(entity.id, entity.email.clone()) + .await; + Ok(entity) + } + + pub async fn update( + account_id: u64, + request: AccountUpdateRequest, + validate: bool, + ) -> RustMailerResult<()> { + if validate { + request.validate_update_request()?; + } + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?.ok_or_else(|| raise_error!(format!( + "Attempted to edit the account's base information, but the corresponding account metadata was not found. account_id={}", + account_id + ), ErrorCode::ResourceNotFound)) + }, |current|{ + Self::apply_update_fields(current, request) + }).await?; + + Ok(()) + } + + pub async fn delete(account_id: u64) -> RustMailerResult<()> { + let request = AccountUpdateRequest { + enabled: Some(false), + ..Default::default() + }; + Self::update(account_id, request, false).await?; + IMAP_TASKS.stop(account_id).await?; + tokio::spawn(async move { + if let Err(e) = Self::cleanup_account_resources_sequential(account_id).await { + error!("Account cleanup failed for {}: {:?}", account_id, e); + } + }); + Ok(()) + } + + async fn delete_account(account_id: u64) -> RustMailerResult<()> { + delete_impl(DB_MANAGER.meta_db(), move|rw|{ + rw.get().secondary::(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(||raise_error!(format!("The account entity with id={account_id} that you want to delete was not found."), ErrorCode::ResourceNotFound)) + }).await + } + + async fn cleanup_account_resources_sequential(account_id: u64) -> RustMailerResult<()> { + let account = Self::get(account_id).await?; + EmailTemplate::remove_account_templates(account_id).await?; + OAuth2AccessToken::try_delete(account_id).await?; + EventHooks::try_delete(account_id).await?; + AccessToken::cleanup_account(account_id).await?; + MailBox::clean(account_id).await?; + AccountRunningState::delete(account_id).await?; + EnvelopeFlagsManager::clean_account(account.id).await?; + RUST_MAIL_CONTEXT.clean_account(account_id).await?; + Self::delete_account(account_id).await?; + info!("Sequential cleanup completed for account: {}", account_id); + Ok(()) + } + + pub async fn update_sync_folders( + account_id: u64, + sync_folders: Vec, + ) -> RustMailerResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!("When trying to update account sync_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) + }, |current|{ + let mut updated = current.clone(); + updated.sync_folders = sync_folders; + Ok(updated) + }).await?; + Ok(()) + } + + pub async fn update_known_folders( + account_id: u64, + known_folders: BTreeSet, + ) -> RustMailerResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!("When trying to update account known_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) + }, |current|{ + let mut updated = current.clone(); + updated.known_folders = known_folders; + Ok(updated) + }).await?; + Ok(()) + } + + pub async fn update_capabilities( + account_id: u64, + capabilities: Vec, + ) -> RustMailerResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV2Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!("When trying to update account capabilities, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) + }, |current|{ + let mut updated = current.clone(); + updated.capabilities = Some(capabilities); + Ok(updated) + }).await?; + Ok(()) + } + + pub async fn update_dsn_capable(account_id: u64, dsn: bool) -> RustMailerResult<()> { + update_impl( + DB_MANAGER.meta_db(), + move |rw| { + rw.get() + .secondary::(AccountV2Key::id, account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!(format!( + "When trying to update account dsn capabilities, the corresponding record was not found. account_id={}", + account_id + ), ErrorCode::ResourceNotFound) + }) + }, + move |current| { + let mut updated = current.clone(); + updated.dsn_capable = Some(dsn); + Ok(updated) + }, + ) + .await?; + Ok(()) + } + + /// Retrieves a list of all `AccountEntity` instances. + pub async fn list_all() -> RustMailerResult> { + list_all_impl(DB_MANAGER.meta_db()).await + } + + pub async fn minimal_list() -> RustMailerResult> { + let result = list_all_impl(DB_MANAGER.meta_db()) + .await? + .into_iter() + .filter(|a: &AccountV2| a.enabled) + .map(|account: AccountV2| MinimalAccount { + id: account.id, + email: account.email.clone(), + }) + .collect::>(); + Ok(result) + } + + pub async fn count() -> RustMailerResult { + count_by_unique_secondary_key_impl::(DB_MANAGER.meta_db(), AccountV2Key::id) + .await + } + + pub async fn paginate_list( + page: Option, + page_size: Option, + desc: Option, + ) -> RustMailerResult> { + paginate_query_primary_scan_all_impl(DB_MANAGER.meta_db(), page, page_size, desc) + .await + .map(DataPage::from) + } + + // This method applies the updates from the request to the old account entity + fn apply_update_fields( + old: &AccountV2, + request: AccountUpdateRequest, + ) -> RustMailerResult { + let mut new = old.clone(); + + if let Some(date_since) = request.date_since { + new.date_since = Some(date_since); + } + + if let Some(name) = &request.name { + new.name = Some(name.clone()); + } + + if let Some(imap) = &request.imap { + if let Some(current_imap) = &mut new.imap { + current_imap.host = imap.host.clone(); + current_imap.port = imap.port.clone(); + current_imap.encryption = imap.encryption.clone(); + current_imap.auth.auth_type = imap.auth.auth_type.clone(); + if let Some(password) = &imap.auth.password { + let encrypted_password = encrypt!(password)?; + current_imap.auth.password = Some(encrypted_password); + } + current_imap.use_proxy = imap.use_proxy; + } + } + + if let Some(smtp) = &request.smtp { + if let Some(current_smtp) = &mut new.smtp { + current_smtp.host = smtp.host.clone(); + current_smtp.port = smtp.port.clone(); + current_smtp.encryption = smtp.encryption.clone(); + current_smtp.auth.auth_type = smtp.auth.auth_type.clone(); + if let Some(password) = &smtp.auth.password { + let encrypted_password = encrypt!(password)?; + current_smtp.auth.password = Some(encrypted_password); + } + current_smtp.use_proxy = smtp.use_proxy; + } + } + + if let Some(mailboxes) = request.sync_folders { + new.sync_folders = mailboxes; + } + if let Some(full_sync_interval_min) = &request.full_sync_interval_min { + new.full_sync_interval_min = Some(*full_sync_interval_min); + } + if let Some(incremental_sync_interval_sec) = &request.incremental_sync_interval_sec { + new.incremental_sync_interval_sec = *incremental_sync_interval_sec; + } + if let Some(enabled) = request.enabled { + new.enabled = enabled; + } + new.updated_at = utc_now!(); + Ok(new) + } +} + +// Will never be used +impl From for Account { + fn from(value: AccountV2) -> Self { + Self { + id: value.id, + imap: value.imap.unwrap(), + smtp: value.smtp.unwrap(), + enabled: value.enabled, + email: value.email, + name: value.name, + minimal_sync: value.minimal_sync.unwrap(), + capabilities: value.capabilities.unwrap(), + dsn_capable: value.dsn_capable, + date_since: value.date_since, + sync_folders: value.sync_folders, + full_sync_interval_min: value.full_sync_interval_min.unwrap(), + incremental_sync_interval_sec: value.incremental_sync_interval_sec, + known_folders: value.known_folders, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +impl From for AccountV2 { + fn from(value: Account) -> Self { + Self { + id: value.id, + imap: Some(value.imap), + smtp: Some(value.smtp), + enabled: value.enabled, + mailer_type: MailerType::ImapSmtp, + email: value.email, + name: value.name, + minimal_sync: Some(value.minimal_sync), + capabilities: Some(value.capabilities), + dsn_capable: value.dsn_capable, + date_since: value.date_since, + sync_folders: value.sync_folders, + full_sync_interval_min: Some(value.full_sync_interval_min), + incremental_sync_interval_sec: value.incremental_sync_interval_sec, + known_folders: value.known_folders, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} diff --git a/src/modules/cache/imap/address.rs b/src/modules/cache/imap/address.rs index 51508b3..09979d9 100644 --- a/src/modules/cache/imap/address.rs +++ b/src/modules/cache/imap/address.rs @@ -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, diff --git a/src/modules/cache/imap/manager.rs b/src/modules/cache/imap/manager.rs index 3c1a697..058c20e 100644 --- a/src/modules/cache/imap/manager.rs +++ b/src/modules/cache/imap/manager.rs @@ -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)>, ) -> 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?; } diff --git a/src/modules/cache/imap/minimal.rs b/src/modules/cache/imap/minimal.rs index bafa3c8..68ff8c3 100644 --- a/src/modules/cache/imap/minimal.rs +++ b/src/modules/cache/imap/minimal.rs @@ -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, diff --git a/src/modules/cache/imap/mod.rs b/src/modules/cache/imap/mod.rs index 41d98a4..ce7ad2f 100644 --- a/src/modules/cache/imap/mod.rs +++ b/src/modules/cache/imap/mod.rs @@ -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 = LazyLock::new(|| { let mut adapter = ModelsAdapter::new(); diff --git a/src/modules/cache/imap/sync/flow.rs b/src/modules/cache/imap/sync/flow.rs index aef0a00..6c1b974 100644 --- a/src/modules/cache/imap/sync/flow.rs +++ b/src/modules/cache/imap/sync/flow.rs @@ -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) -> 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, @@ -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 = 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 = 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, diff --git a/src/modules/cache/imap/sync/mod.rs b/src/modules/cache/imap/sync/mod.rs index a6b6787..d02ba74 100644 --- a/src/modules/cache/imap/sync/mod.rs +++ b/src/modules/cache/imap/sync/mod.rs @@ -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; diff --git a/src/modules/cache/imap/sync/rebuild.rs b/src/modules/cache/imap/sync/rebuild.rs index 6d75b7a..7ce7c94 100644 --- a/src/modules/cache/imap/sync/rebuild.rs +++ b/src/modules/cache/imap/sync/rebuild.rs @@ -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 { @@ -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!( diff --git a/src/modules/cache/imap/sync/sync_folders.rs b/src/modules/cache/imap/sync/sync_folders.rs index 6763076..ec258db 100644 --- a/src/modules/cache/imap/sync/sync_folders.rs +++ b/src/modules/cache/imap/sync/sync_folders.rs @@ -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> { +pub async fn get_sync_folders(account: &AccountV2) -> RustMailerResult> { 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 RustMailerResult RustMailerResult, ) -> 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(()) } diff --git a/src/modules/cache/imap/sync/sync_type.rs b/src/modules/cache/imap/sync/sync_type.rs index 73e1b73..7b95dc0 100644 --- a/src/modules/cache/imap/sync/sync_type.rs +++ b/src/modules/cache/imap/sync/sync_type.rs @@ -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 { +pub async fn determine_sync_type(account: &AccountV2) -> RustMailerResult { 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 diff --git a/src/modules/cache/imap/task.rs b/src/modules/cache/imap/task.rs index f51ec77..ec9268b 100644 --- a/src/modules/cache/imap/task.rs +++ b/src/modules/cache/imap/task.rs @@ -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| { 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); diff --git a/src/modules/cache/imap/thread.rs b/src/modules/cache/imap/thread.rs index 467324e..b02491d 100644 --- a/src/modules/cache/imap/thread.rs +++ b/src/modules/cache/imap/thread.rs @@ -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, }, diff --git a/src/modules/cache/imap/envelope_v2.rs b/src/modules/cache/imap/v2.rs similarity index 100% rename from src/modules/cache/imap/envelope_v2.rs rename to src/modules/cache/imap/v2.rs diff --git a/src/modules/context/executors.rs b/src/modules/context/executors.rs index 2412cee..dcc2347 100644 --- a/src/modules/context/executors.rs +++ b/src/modules/context/executors.rs @@ -7,7 +7,7 @@ use crate::modules::error::code::ErrorCode; use crate::raise_error; use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, context::controller::SYNC_CONTROLLER, error::RustMailerResult, imap::{executor::ImapExecutor, pool::build_imap_pool}, @@ -115,8 +115,8 @@ impl EmailClientExecutors { } pub async fn start_account_syncers(&self) -> RustMailerResult<()> { - let accounts = Account::list_all().await?; - let active_accounts: Vec = accounts.into_iter().filter(|a| a.enabled).collect(); + let accounts = AccountV2::list_all().await?; + let active_accounts: Vec = accounts.into_iter().filter(|a| a.enabled).collect(); if active_accounts.is_empty() { info!("No active accounts found for IMAP initialization."); diff --git a/src/modules/database/manager.rs b/src/modules/database/manager.rs index d36c5ae..9421722 100644 --- a/src/modules/database/manager.rs +++ b/src/modules/database/manager.rs @@ -2,7 +2,8 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; +use crate::modules::account::v2::AccountV2; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::cache::imap::ENVELOPE_MODELS; use crate::modules::context::Initialize; use crate::modules::error::{code::ErrorCode, RustMailerError}; @@ -20,7 +21,7 @@ use tracing::{info, warn}; pub static DB_MANAGER: LazyLock = LazyLock::new(DatabaseManager::new); use crate::modules::{ - account::{entity::Account, status::AccountRunningState}, + account::status::AccountRunningState, autoconfig::CachedMailSettings, cache::disk::CacheItem, database::{batch_insert_impl, list_all_impl}, @@ -85,6 +86,15 @@ impl DatabaseManager { ) //default 128MB .create(&META_MODELS, DATA_DIR_MANAGER.meta_db.clone()) .map_err(Self::handle_database_error)?; + + let rw = database + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw.migrate::() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw.commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + database .compact() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; @@ -127,7 +137,7 @@ impl DatabaseManager { spawn_migration_task!(SystemSetting); spawn_migration_task!(License); spawn_migration_task!(CachedMailSettings); - spawn_migration_task!(Account); + spawn_migration_task!(AccountV2); spawn_migration_task!(EmailTemplate); spawn_migration_task!(Mta); spawn_migration_task!(OAuth2); diff --git a/src/modules/database/mod.rs b/src/modules/database/mod.rs index 4efd046..11a0786 100644 --- a/src/modules/database/mod.rs +++ b/src/modules/database/mod.rs @@ -3,6 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::modules::account::status::AccountRunningState; +use crate::modules::account::v2::AccountV2; use crate::modules::autoconfig::CachedMailSettings; use crate::modules::cache::disk::CacheItem; use crate::modules::error::RustMailerResult; @@ -59,6 +60,7 @@ impl ModelsAdapter { self.register_model::(); self.register_model::(); self.register_model::(); + self.register_model::(); self.register_model::(); self.register_model::(); self.register_model::(); diff --git a/src/modules/database/tests.rs b/src/modules/database/tests.rs index c701ffd..351e73d 100644 --- a/src/modules/database/tests.rs +++ b/src/modules/database/tests.rs @@ -7,7 +7,7 @@ use std::{collections::BTreeMap, path::PathBuf}; use crate::{ id, modules::{ - account::entity::{Account, AccountKey}, + account::{entity::AccountKey, v2::AccountV2}, cache::imap::mailbox::MailBox, database::META_MODELS, hook::{ @@ -28,7 +28,7 @@ async fn test2() { println!("mailbox: {}", mailbox.id) } - let all = Account::minimal_list().await.unwrap(); + let all = AccountV2::minimal_list().await.unwrap(); for mailbox in all { println!("account:{}", mailbox.id) } @@ -41,7 +41,7 @@ fn test3() { .unwrap(); //database.compact().unwrap(); let r_transaction = database.r_transaction().unwrap(); - let entities: Vec = r_transaction + let entities: Vec = r_transaction .scan() .secondary(AccountKey::id) .unwrap() @@ -51,7 +51,7 @@ fn test3() { .unwrap(); println!("{:#?}", entities); - let entities: Vec = r_transaction + let entities: Vec = r_transaction .scan() .primary() .unwrap() @@ -89,12 +89,12 @@ async fn test4() { #[tokio::test] async fn test5() { - let mut account = Account::default(); + let mut account = AccountV2::default(); let id = id!(64); account.id = id; account.save().await.unwrap(); - let account = Account::get(id).await.unwrap(); + let account = AccountV2::get(id).await.unwrap(); println!("{:#?}", account); } diff --git a/src/modules/envelope/extractor.rs b/src/modules/envelope/extractor.rs index 437d86b..f549281 100644 --- a/src/modules/envelope/extractor.rs +++ b/src/modules/envelope/extractor.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::id; -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::cache::imap::flags_to_hash; use crate::modules::cache::imap::mailbox::EnvelopeFlag; use crate::modules::cache::imap::minimal::MinimalEnvelope; diff --git a/src/modules/grpc/service/account/from.rs b/src/modules/grpc/service/account/from.rs index 13e6121..9d57307 100644 --- a/src/modules/grpc/service/account/from.rs +++ b/src/modules/grpc/service/account/from.rs @@ -4,10 +4,11 @@ use crate::modules::{ account::{ - entity::{Account, AuthConfig, AuthType, Encryption, ImapConfig, SmtpConfig}, + entity::{AuthConfig, AuthType, Encryption, ImapConfig, MailerType, SmtpConfig}, payload::{AccountCreateRequest, AccountUpdateRequest, MinimalAccount}, since::{DateSince, RelativeDate, Unit}, status::{AccountError, AccountRunningState}, + v2::AccountV2, }, grpc::service::rustmailer_grpc, }; @@ -208,25 +209,24 @@ impl From for rustmailer_grpc::DateSince { } } -impl TryFrom for Account { +impl TryFrom for AccountV2 { type Error = &'static str; fn try_from(value: rustmailer_grpc::Account) -> Result { - Ok(Account { + Ok(AccountV2 { id: value.id, - imap: value - .imap - .ok_or("IMAP configuration is required")? - .try_into()?, - smtp: value - .smtp - .ok_or("SMTP configuration is required")? - .try_into()?, + imap: value.imap.map(|imap| imap.try_into()).transpose()?, + smtp: value.smtp.map(|smtp| smtp.try_into()).transpose()?, enabled: value.enabled, + mailer_type: value.mailer_type.try_into()?, email: value.email, name: value.name, minimal_sync: value.minimal_sync, - capabilities: value.capabilities, + capabilities: if value.capabilities.is_empty() { + None + } else { + Some(value.capabilities) + }, dsn_capable: value.dsn_capable, date_since: value.date_since.map(|ds| ds.try_into()).transpose()?, sync_folders: value.sync_folders, @@ -239,17 +239,18 @@ impl TryFrom for Account { } } -impl From for rustmailer_grpc::Account { - fn from(value: Account) -> Self { +impl From for rustmailer_grpc::Account { + fn from(value: AccountV2) -> Self { rustmailer_grpc::Account { id: value.id, - imap: Some(value.imap.into()), - smtp: Some(value.smtp.into()), + imap: value.imap.map(|imap| imap.into()), + smtp: value.smtp.map(|smtp| smtp.into()), enabled: value.enabled, + mailer_type: value.mailer_type.into(), email: value.email, name: value.name, minimal_sync: value.minimal_sync, - capabilities: value.capabilities, + capabilities: value.capabilities.unwrap_or_default(), dsn_capable: value.dsn_capable, date_since: value.date_since.map(Into::into), sync_folders: value.sync_folders, @@ -269,15 +270,10 @@ impl TryFrom for AccountCreateRequest { Ok(AccountCreateRequest { email: value.email, name: value.name, - imap: value - .imap - .ok_or("IMAP configuration is required")? - .try_into()?, - smtp: value - .smtp - .ok_or("SMTP configuration is required")? - .try_into()?, + imap: value.imap.map(|imap| imap.try_into()).transpose()?, + smtp: value.smtp.map(|smtp| smtp.try_into()).transpose()?, enabled: value.enabled, + mailer_type: value.mailer_type.try_into()?, date_since: value.date_since.map(|ds| ds.try_into()).transpose()?, minimal_sync: value.minimal_sync, full_sync_interval_min: value.full_sync_interval_min, @@ -340,3 +336,24 @@ impl From for rustmailer_grpc::MinimalAccount { } } } + +impl TryFrom for MailerType { + type Error = &'static str; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(MailerType::ImapSmtp), + 1 => Ok(MailerType::GmailApi), + _ => Err("Invalid value for Unit"), + } + } +} + +impl From for i32 { + fn from(value: MailerType) -> Self { + match value { + MailerType::ImapSmtp => 0, + MailerType::GmailApi => 1, + } + } +} diff --git a/src/modules/grpc/service/account/mod.rs b/src/modules/grpc/service/account/mod.rs index a21e056..e6e3896 100644 --- a/src/modules/grpc/service/account/mod.rs +++ b/src/modules/grpc/service/account/mod.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::account::entity::Account as RustMailerAccount; +use crate::modules::account::v2::AccountV2 as RustMailerAccount; use crate::modules::account::payload::filter_accessible_accounts; use crate::modules::account::payload::AccountCreateRequest as RustMailerAccountCreateRequest; use crate::modules::account::payload::AccountUpdateRequest as RustMailerAccountUpdateRequest; diff --git a/src/modules/grpc/service/message/from.rs b/src/modules/grpc/service/message/from.rs index e06a68b..56daf16 100644 --- a/src/modules/grpc/service/message/from.rs +++ b/src/modules/grpc/service/message/from.rs @@ -5,7 +5,7 @@ use crate::modules::{ cache::imap::{ envelope::Received, - envelope_v2::EmailEnvelopeV2, + v2::EmailEnvelopeV2, mailbox::{EmailFlag, EnvelopeFlag}, }, common::Addr, diff --git a/src/modules/hook/entity.rs b/src/modules/hook/entity.rs index 93593f8..ec15545 100644 --- a/src/modules/hook/entity.rs +++ b/src/modules/hook/entity.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::id; -use crate::modules::account::entity::Account; +use crate::modules::account::v2::AccountV2; use crate::modules::database::manager::DB_MANAGER; use crate::modules::database::{ delete_impl, filter_by_secondary_key_impl, paginate_query_primary_scan_all_impl, @@ -29,7 +29,7 @@ use std::collections::BTreeMap; use std::fmt; use url::Url; -use super::payload::{apply_internal_update, InternalEventHookUpdateRequest}; +use crate::modules::hook::payload::{apply_internal_update, InternalEventHookUpdateRequest}; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Enum)] pub enum HttpMethod { @@ -129,7 +129,7 @@ impl EventHooks { pub async fn new(request: EventhookCreateRequest) -> RustMailerResult { let (email, global) = if let Some(account_id) = request.account_id { - (Some(Account::get(account_id).await?.email), 0) + (Some(AccountV2::get(account_id).await?.email), 0) } else { (None, 1) }; @@ -263,7 +263,7 @@ impl EventHooks { async fn validate(&self) -> RustMailerResult<()> { if let Some(account_id) = self.account_id { - if Account::get(account_id).await?.is_none() { + if AccountV2::get(account_id).await?.is_none() { return Err(raise_error!( format!("Account with id '{}' not exists", account_id), ErrorCode::InvalidParameter diff --git a/src/modules/imap/capabilities.rs b/src/modules/imap/capabilities.rs index 81230c8..09ffc05 100644 --- a/src/modules/imap/capabilities.rs +++ b/src/modules/imap/capabilities.rs @@ -5,7 +5,6 @@ use crate::modules::error::code::ErrorCode; use crate::modules::imap::session::SessionStream; use crate::{modules::error::RustMailerResult, raise_error}; -#[cfg(not(test))] use async_imap::types::Capability; use async_imap::{types::Capabilities, Session}; @@ -27,7 +26,7 @@ pub fn check_capabilities(capabilities: &Capabilities) -> RustMailerResult<()> { } Ok(()) } -#[cfg(not(test))] + pub fn capability_to_string(capability: &Capability) -> String { match capability { Capability::Imap4rev1 => "IMAP4rev1".into(), diff --git a/src/modules/imap/manager.rs b/src/modules/imap/manager.rs index b4ca373..0ef4de0 100644 --- a/src/modules/imap/manager.rs +++ b/src/modules/imap/manager.rs @@ -2,14 +2,14 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -#[cfg(not(test))] use crate::modules::account::dispatcher::STATUS_DISPATCHER; -use crate::modules::account::entity::{Account, AuthType}; +use crate::modules::account::entity::AuthType; +use crate::modules::account::v2::AccountV2; use crate::modules::error::code::ErrorCode; use crate::modules::error::RustMailerResult; -#[cfg(not(test))] -use crate::modules::imap::capabilities::capability_to_string; -use crate::modules::imap::capabilities::{check_capabilities, fetch_capabilities}; +use crate::modules::imap::capabilities::{ + capability_to_string, check_capabilities, fetch_capabilities, +}; use crate::modules::imap::client::Client; use crate::modules::imap::oauth2::OAuth2; use crate::modules::imap::session::SessionStream; @@ -28,29 +28,32 @@ impl ImapConnectionManager { Self { account_id } } - pub async fn fetch_account(&self) -> RustMailerResult { + pub async fn fetch_account(&self) -> RustMailerResult { // Fetch the account entity in non-test environment - Account::get(self.account_id).await + AccountV2::get(self.account_id).await } - async fn create_client(&self, account: &Account) -> RustMailerResult { - Client::connection( - account.imap.host.clone(), - account.imap.encryption.clone(), - account.imap.port, - account.imap.use_proxy, - ) - .await + async fn create_client(&self, account: &AccountV2) -> RustMailerResult { + let imap = account + .imap + .clone() + .expect("BUG: account.imap is None, but it should always be present"); + Client::connection(imap.host, imap.encryption, imap.port, imap.use_proxy).await } async fn authenticate( &self, client: Client, - account: &Account, + account: &AccountV2, ) -> RustMailerResult>> { - match &account.imap.auth.auth_type { + let imap = account + .imap + .clone() + .expect("BUG: account.imap is None, but it should always be present"); + + match &imap.auth.auth_type { AuthType::Password => { - let password = account.imap.auth.password.clone().ok_or_else(|| { + let password = imap.auth.password.clone().ok_or_else(|| { raise_error!( "Imap auth type is Passwd, but password not set".into(), ErrorCode::MissingConfiguration @@ -86,7 +89,6 @@ impl ImapConnectionManager { "Failed to create IMAP {}'s client: {:#?}", &account.email, error ); - #[cfg(not(test))] STATUS_DISPATCHER .append_error( self.account_id, @@ -101,7 +103,7 @@ impl ImapConnectionManager { Ok(session) => session, Err(error) => { error!("Failed to authenticate IMAP session: {:#?}", error); - #[cfg(not(test))] + STATUS_DISPATCHER .append_error( self.account_id, @@ -114,14 +116,10 @@ impl ImapConnectionManager { match fetch_capabilities(&mut session).await { Ok(capabilities) => { - #[cfg(not(test))] let to_save: Vec = capabilities.iter().map(capability_to_string).collect(); - #[cfg(not(test))] - Account::update_capabilities(self.account_id, to_save).await?; - + AccountV2::update_capabilities(self.account_id, to_save).await?; if let Err(error) = check_capabilities(&capabilities) { error!("Failed to check IMAP capabilities: {:#?}", error); - #[cfg(not(test))] STATUS_DISPATCHER .append_error( self.account_id, @@ -133,7 +131,6 @@ impl ImapConnectionManager { } Err(error) => { error!("Failed to fetch IMAP capabilities: {:#?}", error); - #[cfg(not(test))] STATUS_DISPATCHER .append_error( self.account_id, diff --git a/src/modules/license/mod.rs b/src/modules/license/mod.rs index d9ce776..680b8f8 100644 --- a/src/modules/license/mod.rs +++ b/src/modules/license/mod.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::account::entity::Account; +use crate::modules::account::v2::AccountV2; use crate::modules::context::Initialize; use crate::modules::database::manager::DB_MANAGER; use crate::modules::database::{async_find_impl, upsert_impl}; @@ -174,7 +174,7 @@ impl License { let license_str = license_str.into(); let license = match current_license { Some(license) => { - let count = Account::count().await?; + let count = AccountV2::count().await?; if let Some(max_accounts) = license_content.max_accounts { if count > max_accounts as usize { return Err(raise_error!(format!( diff --git a/src/modules/mailbox/create.rs b/src/modules/mailbox/create.rs index dd30edb..9f91ef9 100644 --- a/src/modules/mailbox/create.rs +++ b/src/modules/mailbox/create.rs @@ -3,12 +3,12 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::{ - encode_mailbox_name, modules::account::entity::Account, + encode_mailbox_name, modules::account::v2::AccountV2, modules::context::executors::RUST_MAIL_CONTEXT, modules::error::RustMailerResult, }; pub async fn create_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; executor .create_mailbox(encode_mailbox_name!(mailbox_name).as_str()) diff --git a/src/modules/mailbox/delete.rs b/src/modules/mailbox/delete.rs index 919b483..98ad23b 100644 --- a/src/modules/mailbox/delete.rs +++ b/src/modules/mailbox/delete.rs @@ -3,11 +3,11 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::{encode_mailbox_name, modules::{ - account::entity::Account, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, }}; pub async fn delete_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; executor.delete_mailbox(encode_mailbox_name!(mailbox_name).as_str()).await } diff --git a/src/modules/mailbox/list.rs b/src/modules/mailbox/list.rs index 4e291ce..62995be 100644 --- a/src/modules/mailbox/list.rs +++ b/src/modules/mailbox/list.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::account::entity::Account; +use crate::modules::account::v2::AccountV2; use crate::modules::cache::imap::mailbox::{Attribute, AttributeEnum, MailBox}; use crate::modules::context::executors::RUST_MAIL_CONTEXT; use crate::modules::error::code::ErrorCode; @@ -15,8 +15,8 @@ pub async fn get_account_mailboxes( account_id: u64, remote: bool, ) -> RustMailerResult> { - let account = Account::check_account_active(account_id).await?; - let remote = remote || account.minimal_sync; + let account = AccountV2::check_account_active(account_id).await?; + let remote = remote || account.minimal_sync(); if remote { request_imap_all_mailbox_list(account_id).await } else { @@ -25,7 +25,7 @@ pub async fn get_account_mailboxes( } pub async fn list_subscribed_mailboxes(account_id: u64) -> RustMailerResult> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; request_imap_subscribed_mailbox_list(account_id).await } diff --git a/src/modules/mailbox/rename.rs b/src/modules/mailbox/rename.rs index d8934d9..3500d50 100644 --- a/src/modules/mailbox/rename.rs +++ b/src/modules/mailbox/rename.rs @@ -2,9 +2,12 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::{encode_mailbox_name, modules::{ - account::entity::Account, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, -}}; +use crate::{ + encode_mailbox_name, + modules::{ + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, + }, +}; use poem_openapi::Object; use serde::{Deserialize, Serialize}; @@ -22,9 +25,12 @@ pub async fn rename_mailbox( account_id: u64, payload: MailboxRenameRequest, ) -> RustMailerResult<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; executor - .rename_mailbox(encode_mailbox_name!(&payload.current_name).as_str(), encode_mailbox_name!(&payload.new_name).as_str()) + .rename_mailbox( + encode_mailbox_name!(&payload.current_name).as_str(), + encode_mailbox_name!(&payload.new_name).as_str(), + ) .await } diff --git a/src/modules/mailbox/subscribe.rs b/src/modules/mailbox/subscribe.rs index f0a5355..c26e4ee 100644 --- a/src/modules/mailbox/subscribe.rs +++ b/src/modules/mailbox/subscribe.rs @@ -5,12 +5,12 @@ use crate::{ encode_mailbox_name, modules::{ - account::entity::Account, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, }, }; pub async fn subscribe_mailbox(account_id: u64, mailbox_name: &str) -> RustMailerResult<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).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<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; executor .unsubscribe_mailbox(encode_mailbox_name!(mailbox_name).as_str()) diff --git a/src/modules/message/append.rs b/src/modules/message/append.rs index db9321a..d698462 100644 --- a/src/modules/message/append.rs +++ b/src/modules/message/append.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use crate::{ encode_mailbox_name, modules::{ - account::entity::Account, + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::{code::ErrorCode, RustMailerResult}, smtp::request::{reply::apply_references, EmailHandler}, @@ -51,7 +51,7 @@ pub struct AppendReplyToDraftRequest { impl AppendReplyToDraftRequest { pub async fn append_reply_to_draft(&self, account_id: u64) -> RustMailerResult<()> { - let account = Account::check_account_active(account_id).await?; + let account = AccountV2::check_account_active(account_id).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())), diff --git a/src/modules/message/attachment.rs b/src/modules/message/attachment.rs index 2a2e278..7f5cc36 100644 --- a/src/modules/message/attachment.rs +++ b/src/modules/message/attachment.rs @@ -6,7 +6,7 @@ use crate::modules::error::code::ErrorCode; use crate::modules::message::get_minimal_meta; use crate::{ encode_mailbox_name, - modules::account::entity::Account, + modules::account::v2::AccountV2, modules::cache::disk::DISK_CACHE, modules::context::executors::RUST_MAIL_CONTEXT, modules::error::RustMailerResult, @@ -59,7 +59,7 @@ pub async fn retrieve_email_attachment( account_id: u64, request: AttachmentRequest, ) -> RustMailerResult { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; if request.attachment.size >= MAX_ATTACHMENT_SIZE { return Err(raise_error!( diff --git a/src/modules/message/content.rs b/src/modules/message/content.rs index 93bd188..8dda5d0 100644 --- a/src/modules/message/content.rs +++ b/src/modules/message/content.rs @@ -2,13 +2,13 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. +use crate::modules::account::v2::AccountV2; use crate::modules::error::code::ErrorCode; use crate::modules::imap::section::Encoding; use crate::modules::message::attachment::inline_attachment_diskcache_key; use crate::{ encode_mailbox_name, modules::{ - account::entity::Account, cache::disk::DISK_CACHE, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, @@ -283,7 +283,7 @@ pub async fn retrieve_email_content( request: MessageContentRequest, skip_cache: bool, ) -> RustMailerResult { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let mut plain: Option = None; let mut html: Option<String> = None; diff --git a/src/modules/message/copy.rs b/src/modules/message/copy.rs index f278e4a..c18e268 100644 --- a/src/modules/message/copy.rs +++ b/src/modules/message/copy.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::{ - encode_mailbox_name, modules::account::entity::Account, + encode_mailbox_name, modules::account::v2::AccountV2, modules::context::executors::RUST_MAIL_CONTEXT, modules::envelope::generate_uid_set, modules::error::RustMailerResult, }; @@ -33,7 +33,7 @@ pub async fn copy_mailbox_messages( payload: &MailboxTransferRequest, ) -> RustMailerResult<()> { // Ensure the account exists before proceeding - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; // Generate a set of UIDs from the payload let uid_set = generate_uid_set(payload.uids.clone()); diff --git a/src/modules/message/delete.rs b/src/modules/message/delete.rs index 06f8f8a..4874ab2 100644 --- a/src/modules/message/delete.rs +++ b/src/modules/message/delete.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::encode_mailbox_name; -use crate::modules::account::entity::Account; +use crate::modules::account::v2::AccountV2; use crate::modules::cache::imap::mailbox::{AttributeEnum, MailBox}; use crate::modules::context::executors::RUST_MAIL_CONTEXT; use crate::modules::{envelope::generate_uid_set, error::RustMailerResult}; @@ -25,7 +25,7 @@ pub async fn move_to_trash_or_delete_messages_directly( account_id: u64, request: &MessageDeleteRequest, ) -> RustMailerResult<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let uid_set = generate_uid_set(request.uids.clone()); let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; diff --git a/src/modules/message/flag.rs b/src/modules/message/flag.rs index 1905b01..67d11db 100644 --- a/src/modules/message/flag.rs +++ b/src/modules/message/flag.rs @@ -4,7 +4,7 @@ use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, cache::imap::mailbox::EnvelopeFlag, context::executors::RUST_MAIL_CONTEXT, envelope::generate_uid_set, @@ -81,7 +81,7 @@ impl FlagAction { } pub async fn modify_flags(account_id: u64, request: FlagMessageRequest) -> RustMailerResult<()> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; request.validate()?; let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; diff --git a/src/modules/message/full.rs b/src/modules/message/full.rs index a8ad2b8..2d124c7 100644 --- a/src/modules/message/full.rs +++ b/src/modules/message/full.rs @@ -4,7 +4,7 @@ use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, cache::disk::DISK_CACHE, context::executors::RUST_MAIL_CONTEXT, error::{code::ErrorCode, RustMailerResult}, @@ -35,7 +35,7 @@ pub async fn retrieve_full_email( mailbox: String, uid: u32, ) -> RustMailerResult<cacache::Reader> { - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; let meta = get_minimal_meta(account_id, &mailbox, uid).await?; if meta.size > MAX_EMAIL_TOTAL_SIZE { return Err(raise_error!(format!( diff --git a/src/modules/message/list.rs b/src/modules/message/list.rs index 2fb47ee..47e62ff 100644 --- a/src/modules/message/list.rs +++ b/src/modules/message/list.rs @@ -5,8 +5,8 @@ use crate::{ encode_mailbox_name, modules::{ - account::entity::Account, - cache::imap::{envelope_v2::EmailEnvelopeV2, mailbox::MailBox, thread::EmailThread}, + account::v2::AccountV2, + cache::imap::{mailbox::MailBox, thread::EmailThread, v2::EmailEnvelopeV2}, context::executors::RUST_MAIL_CONTEXT, envelope::extractor::extract_envelope, error::{code::ErrorCode, RustMailerResult}, @@ -24,9 +24,9 @@ pub async fn list_messages_in_mailbox( remote: bool, desc: bool, ) -> RustMailerResult<DataPage<EmailEnvelopeV2>> { - let account = Account::check_account_active(account_id).await?; + let account = AccountV2::check_account_active(account_id).await?; validate_pagination_params(page, page_size)?; - let remote = remote || account.minimal_sync; + let remote = remote || account.minimal_sync(); if remote { fetch_remote_messages(account_id, mailbox_name, page, page_size, desc).await @@ -99,7 +99,7 @@ async fn process_fetches( } async fn fetch_local_messages( - account: &Account, + account: &AccountV2, mailbox_name: &str, page: u64, page_size: u64, @@ -125,9 +125,9 @@ pub async fn list_threads_in_mailbox( page_size: u64, desc: bool, ) -> RustMailerResult<DataPage<EmailEnvelopeV2>> { - let account = Account::check_account_active(account_id).await?; + let account = AccountV2::check_account_active(account_id).await?; validate_pagination_params(page, page_size)?; - if account.minimal_sync { + if account.minimal_sync() { return Err(raise_error!( format!( "Account {} is in minimal sync mode. Listing threads in a mailbox is not supported. \ @@ -160,8 +160,8 @@ pub async fn get_thread_messages( mailbox_name: &str, thread_id: u64, ) -> RustMailerResult<Vec<EmailEnvelopeV2>> { - let account = Account::check_account_active(account_id).await?; - if account.minimal_sync { + let account = AccountV2::check_account_active(account_id).await?; + if account.minimal_sync() { return Err(raise_error!( format!( "Account {} is in minimal sync mode. Listing threads in a mailbox is not supported. \ diff --git a/src/modules/message/mv.rs b/src/modules/message/mv.rs index aab9866..ddbed9b 100644 --- a/src/modules/message/mv.rs +++ b/src/modules/message/mv.rs @@ -4,7 +4,7 @@ use crate::modules::message::copy::MailboxTransferRequest; use crate::{ - encode_mailbox_name, modules::account::entity::Account, + encode_mailbox_name, modules::account::v2::AccountV2, modules::context::executors::RUST_MAIL_CONTEXT, modules::envelope::generate_uid_set, modules::error::RustMailerResult, }; @@ -14,7 +14,7 @@ pub async fn move_mailbox_messages( payload: &MailboxTransferRequest, ) -> RustMailerResult<()> { // Ensure the account exists before proceeding - Account::check_account_active(account_id).await?; + AccountV2::check_account_active(account_id).await?; // Generate a set of UIDs from the payload let uid_set = generate_uid_set(payload.uids.clone()); diff --git a/src/modules/message/search/payload.rs b/src/modules/message/search/payload.rs index 3003bf2..1c3fe4a 100644 --- a/src/modules/message/search/payload.rs +++ b/src/modules/message/search/payload.rs @@ -3,8 +3,8 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::modules::cache::imap::address::AddressEntity; -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; use crate::modules::cache::imap::sync::flow::generate_uid_sequence_hashset; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::common::paginated::paginate_vec; use crate::modules::database::Paginated; use crate::modules::error::code::ErrorCode; @@ -12,7 +12,7 @@ use crate::modules::message::search::cache::IMAP_SEARCH_CACHE; use crate::{ encode_mailbox_name, modules::{ - account::entity::Account, context::executors::RUST_MAIL_CONTEXT, + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, envelope::extractor::extract_envelope, error::RustMailerResult, rest::response::DataPage, }, raise_error, @@ -437,13 +437,13 @@ impl MessageSearchRequest { page_size: u64, desc: bool, ) -> RustMailerResult<DataPage<EmailEnvelopeV2>> { - let account = Account::check_account_active(account_id).await?; + let account = AccountV2::check_account_active(account_id).await?; self.search_remote(&account, page, page_size, desc).await } async fn search_remote( &self, - account: &Account, + account: &AccountV2, page: u64, page_size: u64, desc: bool, diff --git a/src/modules/overview/mod.rs b/src/modules/overview/mod.rs index 2446dfc..17ea152 100644 --- a/src/modules/overview/mod.rs +++ b/src/modules/overview/mod.rs @@ -6,7 +6,7 @@ use poem_openapi::Object; use serde::{Deserialize, Serialize}; use crate::modules::{ - account::entity::Account, + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::RustMailerResult, metrics::{ @@ -51,7 +51,7 @@ impl Overview { let pending_hook_tasks = send_queue .list_hook_tasks_by_status(TaskStatus::Scheduled) .await?; - let account_num = Account::count().await?; + let account_num = AccountV2::count().await?; let mut time_series = MetricsTimeSeries::get().await?; time_series.sort_by_timestamp(); diff --git a/src/modules/rest/api/account.rs b/src/modules/rest/api/account.rs index 2b12871..16fbb74 100644 --- a/src/modules/rest/api/account.rs +++ b/src/modules/rest/api/account.rs @@ -4,11 +4,11 @@ use std::collections::BTreeSet; -use crate::modules::account::entity::Account; use crate::modules::account::payload::{ filter_accessible_accounts, AccountCreateRequest, AccountUpdateRequest, MinimalAccount, }; use crate::modules::account::status::AccountRunningState; +use crate::modules::account::v2::AccountV2; use crate::modules::common::auth::ClientContext; use crate::modules::common::paginated::paginate_vec; use crate::modules::error::code::ErrorCode; @@ -37,11 +37,11 @@ impl AccountApi { /// The account ID to retrieve account_id: Path<u64>, context: ClientContext, - ) -> ApiResult<Json<Account>> { + ) -> ApiResult<Json<AccountV2>> { let account_id = account_id.0; context.require_account_access(account_id)?; println!("{}", account_id); - Ok(Json(Account::get(account_id).await?)) + Ok(Json(AccountV2::get(account_id).await?)) } /// Delete an account by ID - WARNING: This permanently removes the account and all associated resources @@ -58,7 +58,7 @@ impl AccountApi { ) -> ApiResult<()> { let account_id = account_id.0; context.require_account_access(account_id)?; - Ok(Account::delete(account_id).await?) + Ok(AccountV2::delete(account_id).await?) } /// Create a new account @@ -68,8 +68,8 @@ impl AccountApi { /// Account creation request payload payload: Json<AccountCreateRequest>, context: ClientContext, - ) -> ApiResult<Json<Account>> { - let account = Account::create_account(payload.0).await?; + ) -> ApiResult<Json<AccountV2>> { + let account = AccountV2::create_account(payload.0).await?; if let Some(access_token) = &context.access_token { let account_info = AccountInfo { id: account.id, @@ -96,7 +96,7 @@ impl AccountApi { ) -> ApiResult<()> { let account_id = account_id.0; context.require_account_access(account_id)?; - Ok(Account::update(account_id, payload.0, true).await?) + Ok(AccountV2::update(account_id, payload.0, true).await?) } /// List accounts with optional pagination parameters @@ -114,20 +114,20 @@ impl AccountApi { /// Optional. Whether to sort the list in descending order. desc: Query<Option<bool>>, context: ClientContext, - ) -> ApiResult<Json<DataPage<Account>>> { + ) -> ApiResult<Json<DataPage<AccountV2>>> { let accessible_accounts = context.accessible_accounts()?; if accessible_accounts.is_none() { return Ok(Json( - Account::paginate_list(page.0, page_size.0, desc.0).await?, + AccountV2::paginate_list(page.0, page_size.0, desc.0).await?, )); } - let all_accounts = Account::list_all().await?; + let all_accounts = AccountV2::list_all().await?; let allowed_ids: BTreeSet<u64> = accessible_accounts.unwrap().iter().map(|a| a.id).collect(); - let mut filtered_accounts: Vec<Account> = all_accounts + let mut filtered_accounts: Vec<AccountV2> = all_accounts .into_iter() .filter(|acct| allowed_ids.contains(&acct.id)) .collect(); @@ -183,7 +183,7 @@ impl AccountApi { ) -> ApiResult<Json<Vec<MinimalAccount>>> { let accessible_accounts = context.accessible_accounts()?; - let minimal_list = Account::minimal_list().await?; + let minimal_list = AccountV2::minimal_list().await?; let result = match accessible_accounts { Some(set) => filter_accessible_accounts(&minimal_list, set), None => minimal_list, diff --git a/src/modules/rest/api/message.rs b/src/modules/rest/api/message.rs index 5e8a14c..50ef2c2 100644 --- a/src/modules/rest/api/message.rs +++ b/src/modules/rest/api/message.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::current_datetime; -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::common::auth::ClientContext; use crate::modules::message::append::AppendReplyToDraftRequest; use crate::modules::message::attachment::{retrieve_email_attachment, AttachmentRequest}; diff --git a/src/modules/smtp/composer/mod.rs b/src/modules/smtp/composer/mod.rs index ed83fe2..0c5af80 100644 --- a/src/modules/smtp/composer/mod.rs +++ b/src/modules/smtp/composer/mod.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use scraper::{Html, Selector}; use time::{macros::format_description, OffsetDateTime}; use time_tz::timezones; @@ -229,7 +229,7 @@ mod tests { id, modules::{ cache::imap::{ - envelope_v2::EmailEnvelopeV2, + v2::EmailEnvelopeV2, mailbox::{EmailFlag, EnvelopeFlag}, }, common::Addr, diff --git a/src/modules/smtp/manager.rs b/src/modules/smtp/manager.rs index a389db7..bf17dbb 100644 --- a/src/modules/smtp/manager.rs +++ b/src/modules/smtp/manager.rs @@ -3,13 +3,14 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::modules::account::entity::{AuthType, Encryption}; +use crate::modules::account::v2::AccountV2; use crate::modules::error::code::ErrorCode; +use crate::modules::error::RustMailerResult; use crate::modules::oauth2::token::OAuth2AccessToken; use crate::modules::settings::proxy::Proxy; use crate::modules::smtp::client::RustMailSmtpClient; use crate::modules::smtp::mta::entity::Mta; use crate::modules::utils::net::parse_proxy_addr; -use crate::modules::{account::entity::Account, error::RustMailerResult}; use crate::{decrypt, raise_error}; use mail_send::smtp::tls::build_tls_connector; use mail_send::smtp::AssertReply; @@ -103,10 +104,16 @@ impl SmtpClientManager { } async fn build_client(account_id: u64) -> RustMailerResult<RustMailSmtpClient> { - let account = Account::get(account_id).await?; - let credentials = match &account.smtp.auth.auth_type { + let account = AccountV2::get(account_id).await?; + + let smtp = account + .smtp + .as_ref() + .expect("BUG: account.smtp is None, but it should always be present here"); + + let credentials = match smtp.auth.auth_type { AuthType::Password => { - let password = account.smtp.auth.password.as_ref().ok_or_else(|| { + let password = smtp.auth.password.as_ref().ok_or_else(|| { raise_error!( "smtp auth type is Password, but password not set".into(), ErrorCode::MissingConfiguration @@ -129,20 +136,17 @@ impl SmtpClientManager { }; let timeout = Duration::from_secs(30); - if let Some(proxy_id) = &account.smtp.use_proxy { - let proxy = Proxy::get(*proxy_id).await?; + if let Some(proxy_id) = smtp.use_proxy { + let proxy = Proxy::get(proxy_id).await?; let proxy = parse_proxy_addr(&proxy.url)?; - let socks_stream = Socks5Stream::connect( - proxy, - format!("{}:{}", &account.smtp.host, account.smtp.port), - ) - .await - .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let socks_stream = Socks5Stream::connect(proxy, format!("{}:{}", smtp.host, smtp.port)) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; let tcp_stream = socks_stream.into_inner(); return Self::connect( - account.smtp.encryption, - &account.smtp.host, + smtp.encryption.clone(), + &smtp.host, timeout, tcp_stream, credentials, @@ -151,11 +155,11 @@ impl SmtpClientManager { .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::SmtpConnectionFailed)); } - let builder = SmtpClientBuilder::new(account.smtp.host, account.smtp.port) + let builder = SmtpClientBuilder::new(smtp.host.clone(), smtp.port) .credentials(credentials) .timeout(timeout); - let client = match account.smtp.encryption { + let client = match smtp.encryption { Encryption::Ssl => { let client = builder.implicit_tls(true).connect().await.map_err(|e| { raise_error!(format!("{:#?}", e), ErrorCode::SmtpConnectionFailed) diff --git a/src/modules/smtp/request/forward.rs b/src/modules/smtp/request/forward.rs index 799c38e..ac6e7b0 100644 --- a/src/modules/smtp/request/forward.rs +++ b/src/modules/smtp/request/forward.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::error::code::ErrorCode; use crate::modules::smtp::request::builder::EmailBuilder; use crate::modules::smtp::request::headers::HeaderValue; @@ -13,7 +13,7 @@ use crate::modules::smtp::util::generate_message_id; use crate::validate_email; use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, error::RustMailerResult, smtp::{ composer::BodyComposer, @@ -153,7 +153,7 @@ impl EmailBuilder for ForwardEmailRequest { async fn build(&self, account_id: u64) -> RustMailerResult<()> { self.validate().await?; - let account = &Account::get(account_id).await?; + let account = &AccountV2::get(account_id).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())), @@ -241,7 +241,7 @@ impl ForwardEmailRequest { &self, mut builder: MessageBuilder<'static>, envelope: &EmailEnvelopeV2, - account: &Account, + account: &AccountV2, ) -> RustMailerResult<MessageBuilder<'static>> { let timezone = self.timezone.as_deref().unwrap_or("UTC"); @@ -316,7 +316,7 @@ impl ForwardEmailRequest { async fn apply_attachments( &self, mut builder: MessageBuilder<'static>, - account: &Account, + account: &AccountV2, ) -> RustMailerResult<MessageBuilder<'static>> { if let Some(attachments) = &self.attachments { for attachment in attachments { diff --git a/src/modules/smtp/request/mod.rs b/src/modules/smtp/request/mod.rs index 152c54d..61f0a7a 100644 --- a/src/modules/smtp/request/mod.rs +++ b/src/modules/smtp/request/mod.rs @@ -5,10 +5,10 @@ use crate::encode_mailbox_name; use crate::generate_token; use crate::modules::cache::disk::DISK_CACHE; -use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2; use crate::modules::cache::imap::mailbox::EmailFlag; use crate::modules::cache::imap::mailbox::EnvelopeFlag; use crate::modules::cache::imap::mailbox::MailBox; +use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::common::Addr; use crate::modules::context::executors::RUST_MAIL_CONTEXT; use crate::modules::envelope::extractor::extract_envelope; @@ -23,7 +23,7 @@ use crate::validate_email; use crate::{ base64_decode_safe, modules::{ - account::entity::Account, + account::v2::AccountV2, error::RustMailerResult, imap::section::ImapAttachment, message::attachment::{retrieve_email_attachment, AttachmentRequest}, @@ -154,7 +154,7 @@ pub struct MailAttachment { } impl MailAttachment { - pub async fn get_content(&self, account: &Account) -> RustMailerResult<BodyPart<'static>> { + pub async fn get_content(&self, account: &AccountV2) -> RustMailerResult<BodyPart<'static>> { if let Some(content) = &self.payload.base64_content { return Self::decode_base64_content(content, &self.mime_type); } @@ -204,7 +204,7 @@ impl MailAttachment { async fn retrieve_and_decode_attachment( attachment_ref: &AttachmentRef, mime_type: &str, - account: &Account, + account: &AccountV2, ) -> RustMailerResult<BodyPart<'static>> { let mut reader = retrieve_email_attachment( account.id, @@ -600,7 +600,7 @@ impl EmailHandler { } pub async fn retrieve_message_content( - account: &Account, + account: &AccountV2, envelope: &EmailEnvelopeV2, ) -> RustMailerResult<Option<MessageContent>> { let body_meta = match &envelope.body_meta { @@ -626,12 +626,12 @@ impl EmailHandler { } pub async fn get_envelope( - account: &Account, + account: &AccountV2, mailbox_name: &str, uid: u32, ) -> RustMailerResult<EmailEnvelopeV2> { if let Ok(mailbox) = MailBox::get(account.id, mailbox_name).await { - if !account.minimal_sync { + if !account.minimal_sync() { let envelope = EmailEnvelopeV2::find(account.id, mailbox.id, uid).await?; if let Some(envelope) = envelope { return Ok(envelope); @@ -705,7 +705,7 @@ impl EmailHandler { attachment: &ImapAttachment, envelope: &EmailEnvelopeV2, inline: bool, - account: &Account, + account: &AccountV2, ) -> RustMailerResult<MessageBuilder<'static>> { let attachment_ref = AttachmentRef { mailbox_name: envelope.mailbox_name.clone(), @@ -747,7 +747,7 @@ impl EmailHandler { } pub async fn schedule_task( - account: &Account, + account: &AccountV2, subject: Option<String>, message_id: String, cc: Option<Vec<EmailAddress>>, diff --git a/src/modules/smtp/request/new.rs b/src/modules/smtp/request/new.rs index 353bab0..44f5282 100644 --- a/src/modules/smtp/request/new.rs +++ b/src/modules/smtp/request/new.rs @@ -4,7 +4,7 @@ use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, error::{code::ErrorCode, RustMailerResult}, settings::cli::SETTINGS, smtp::{ @@ -197,7 +197,7 @@ impl EmailBuilder for SendEmailRequest { async fn build(&self, account_id: u64) -> RustMailerResult<()> { self.validate().await?; - let account = &Account::get(account_id).await?; + let account = &AccountV2::get(account_id).await?; let from = self.from.clone().map(Into::into).unwrap_or_else(|| { Address::new_address( account.name.as_ref().map(|n| Cow::Owned(n.to_string())), @@ -330,7 +330,7 @@ impl SendEmailRequest { &self, mut builder: MessageBuilder<'static>, recipient: &Recipient, - account: &Account, + account: &AccountV2, tracker: Option<EmailTracker>, ) -> RustMailerResult<MessageBuilder<'static>> { if let Some(attachments) = &self.attachments { @@ -432,7 +432,7 @@ impl SendEmailRequest { async fn apply_mail_attachments( mut builder: MessageBuilder<'static>, attachments: &[MailAttachment], - account: &Account, + account: &AccountV2, ) -> RustMailerResult<MessageBuilder<'static>> { for attachment in attachments { let content = attachment.get_content(account).await?; diff --git a/src/modules/smtp/request/reply.rs b/src/modules/smtp/request/reply.rs index 3e6ef1e..d3ac005 100644 --- a/src/modules/smtp/request/reply.rs +++ b/src/modules/smtp/request/reply.rs @@ -4,8 +4,8 @@ use crate::{ modules::{ - account::entity::Account, - cache::imap::envelope_v2::EmailEnvelopeV2, + account::v2::AccountV2, + cache::imap::v2::EmailEnvelopeV2, error::{code::ErrorCode, RustMailerResult}, smtp::{ composer::BodyComposer, @@ -127,7 +127,7 @@ impl EmailBuilder for ReplyEmailRequest { } async fn build(&self, account_id: u64) -> RustMailerResult<()> { - let account = &Account::get(account_id).await?; + let account = &AccountV2::get(account_id).await?; let envelope = EmailHandler::get_envelope(account, &self.mailbox_name, self.uid).await?; let from = Address::new_address( @@ -227,7 +227,7 @@ impl ReplyEmailRequest { &self, mut builder: MessageBuilder<'static>, envelope: &EmailEnvelopeV2, - account: &Account, + account: &AccountV2, ) -> RustMailerResult<MessageBuilder<'static>> { let timezone = self.timezone.as_deref().unwrap_or("UTC"); @@ -302,7 +302,7 @@ impl ReplyEmailRequest { async fn apply_attachments( &self, mut builder: MessageBuilder<'static>, - account: &Account, + account: &AccountV2, ) -> RustMailerResult<MessageBuilder<'static>> { if let Some(attachments) = &self.attachments { for attachment in attachments { diff --git a/src/modules/smtp/request/task.rs b/src/modules/smtp/request/task.rs index b73c968..b7ca1ea 100644 --- a/src/modules/smtp/request/task.rs +++ b/src/modules/smtp/request/task.rs @@ -30,7 +30,7 @@ use crate::modules::smtp::{ request::{EmailHandler, MailEnvelope, SendControl, Strategy}, }; -use crate::modules::{account::entity::Account, context::executors::RUST_MAIL_CONTEXT}; +use crate::modules::{account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT}; use mail_send::smtp::message::{Address, Message, Parameters}; use serde::{Deserialize, Serialize}; @@ -144,15 +144,15 @@ impl Task for SmtpTask { (executor, params) } None => { - let account = Account::get(self.account_id).await?; + let account = AccountV2::get(self.account_id).await?; let executor = RUST_MAIL_CONTEXT.smtp(account.id).await?; let dsn_capable = if let Some(dsn_capable) = &account.dsn_capable { *dsn_capable } else { - let capabilities = executor.capabilities(&account.smtp.host).await?; + let capabilities = executor.capabilities(&account.smtp.as_ref().expect("BUG: account.smtp is None, but it should always be present at this point").host).await?; let dsn_capable = capabilities & EXT_DSN != 0; - Account::update_dsn_capable(account.id, dsn_capable).await?; + AccountV2::update_dsn_capable(account.id, dsn_capable).await?; dsn_capable }; diff --git a/src/modules/smtp/template/entity.rs b/src/modules/smtp/template/entity.rs index c291ac3..3e3d2e2 100644 --- a/src/modules/smtp/template/entity.rs +++ b/src/modules/smtp/template/entity.rs @@ -2,6 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. +use crate::modules::account::v2::AccountV2; use crate::modules::database::manager::DB_MANAGER; use crate::modules::database::{ batch_delete_impl, delete_impl, paginate_query_primary_scan_all_impl, @@ -13,10 +14,7 @@ use crate::modules::rest::response::DataPage; use crate::modules::smtp::template::payload::{TemplateCreateRequest, TemplateUpdateRequest}; use crate::modules::token::AccountInfo; use crate::{id, raise_error}; -use crate::{ - modules::account::entity::Account, modules::database::insert_impl, - modules::error::RustMailerResult, utc_now, -}; +use crate::{modules::database::insert_impl, modules::error::RustMailerResult, utc_now}; use handlebars::Handlebars; use itertools::Itertools; use native_db::*; @@ -78,7 +76,7 @@ impl EmailTemplate { pub async fn new(value: TemplateCreateRequest) -> RustMailerResult<Self> { let account_info = if let Some(account_id) = value.account_id { - Account::get(account_id).await.map(|account| { + AccountV2::get(account_id).await.map(|account| { Some(AccountInfo { id: account_id, email: account.email, @@ -217,7 +215,7 @@ impl EmailTemplate { } async fn check_account_id(account_id: u64) -> RustMailerResult<()> { - let _ = Account::get(account_id).await?; + let _ = AccountV2::get(account_id).await?; Ok(()) } diff --git a/src/modules/smtp/template/send.rs b/src/modules/smtp/template/send.rs index f969c26..24b2c42 100644 --- a/src/modules/smtp/template/send.rs +++ b/src/modules/smtp/template/send.rs @@ -11,7 +11,7 @@ use mail_send::{ use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, context::executors::RUST_MAIL_CONTEXT, error::{code::ErrorCode, RustMailerResult}, smtp::{ @@ -35,7 +35,7 @@ pub async fn send_template_test_email( } = reqwest; let template = EmailTemplate::get(template_id).await?; - let account = Account::get(account_id).await?; + let account = AccountV2::get(account_id).await?; let (subject, text, html) = Templates::render(&template, &template_params)?; diff --git a/src/modules/token/mod.rs b/src/modules/token/mod.rs index f9f7254..7210214 100644 --- a/src/modules/token/mod.rs +++ b/src/modules/token/mod.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::account::entity::Account; +use crate::modules::account::v2::AccountV2; use crate::modules::database::delete_impl; use crate::modules::database::manager::DB_MANAGER; use crate::modules::database::{insert_impl, list_all_impl, update_impl}; @@ -201,7 +201,7 @@ impl AccessToken { let account_infos = if let Some(accounts) = &request.accounts { let mut account_infos = BTreeSet::new(); for account_id in accounts { - let account = Account::get(*account_id).await?; + let account = AccountV2::get(*account_id).await?; account_infos.insert(AccountInfo { id: *account_id, email: account.email, @@ -268,7 +268,7 @@ impl AccessToken { let mut account_infos = BTreeSet::new(); for &account_id in &accounts { - let account = Account::get(account_id).await?; + let account = AccountV2::get(account_id).await?; account_infos.insert(AccountInfo { id: account_id, email: account.email, diff --git a/src/modules/token/payload.rs b/src/modules/token/payload.rs index 9cd85fd..b4d0e57 100644 --- a/src/modules/token/payload.rs +++ b/src/modules/token/payload.rs @@ -6,7 +6,7 @@ use std::collections::BTreeSet; use crate::{ modules::{ - account::entity::Account, + account::v2::AccountV2, error::{code::ErrorCode, RustMailerResult}, token::{AccessControl, AccessTokenScope}, }, @@ -44,7 +44,7 @@ impl AccessTokenCreateRequest { let mut not_found = Vec::new(); for account_id in &self.accounts { - if Account::find(*account_id).await?.is_none() { + if AccountV2::find(*account_id).await?.is_none() { not_found.push(*account_id); } } @@ -88,7 +88,7 @@ impl AccessTokenUpdateRequest { let mut not_found = Vec::new(); for account_id in accounts { - if Account::find(*account_id).await?.is_none() { + if AccountV2::find(*account_id).await?.is_none() { not_found.push(*account_id); } } diff --git a/src/modules/vendor/mod.rs b/src/modules/vendor/mod.rs new file mode 100644 index 0000000..6897cce --- /dev/null +++ b/src/modules/vendor/mod.rs @@ -0,0 +1,9 @@ +// Copyright © 2025 rustmailer.com +// Licensed under RustMailer License Agreement v1.0 +// Unauthorized copying, modification, or distribution is prohibited. + + +#[cfg(test)] +mod tests; + + diff --git a/src/modules/vendor/tests.rs b/src/modules/vendor/tests.rs new file mode 100644 index 0000000..d3a0f31 --- /dev/null +++ b/src/modules/vendor/tests.rs @@ -0,0 +1,44 @@ +// Copyright © 2025 rustmailer.com +// Licensed under RustMailer License Agreement v1.0 +// Unauthorized copying, modification, or distribution is prohibited. + +use reqwest::header::{AUTHORIZATION, CONTENT_TYPE}; +use serde_json::Value; +use std::time::Duration; + +use crate::rustmailer_version; + +#[tokio::test] +async fn test1() { + let access_token = "ya29.A0AS3H6Nx8aJrWEN2JG6zsSmhp_wg9BSS6i0uPoEbgPsdfdPtEydMK25ne2P0mL7FVu6g_F4rpTIDZxi4CO96LtlWokbTPd69vaVHx07qJfnRchq3lrFBLpm-OPqV0zLXbJ6X9VSsVW0Cd5W5ykSYjkQMXUA4u7iM7bpjps49VQo10_-y4l_FO7L28-Q0HGRCvdXJNRqgaaCgYKAVsSARUSFQHGX2Mi9nJ8LfBUMLimPBr_wEahxg0207"; + let url = "https://gmail.googleapis.com/gmail/v1/users/me/labels"; + 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 mut builder = reqwest::ClientBuilder::new() + .user_agent(rustmailer_version!()) + .timeout(Duration::from_secs(10)) + .connect_timeout(Duration::from_secs(10)); + + let proxy_obj = reqwest::Proxy::all("socks5://127.0.0.1:22308").unwrap(); + builder = builder + .redirect(reqwest::redirect::Policy::none()) + .proxy(proxy_obj); + let client = builder.build().unwrap(); + let res = client + .get(url) + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .header(CONTENT_TYPE, "application/json") + .send() + .await + .unwrap(); + + if res.status().is_success() { + let body: Value = res.json().await.unwrap(); + let pretty = serde_json::to_string_pretty(&body).unwrap(); + println!("Response = {}", pretty); + } else { + eprintln!("Error: {} - {:?}", res.status(), res.text().await.unwrap()); + } +} diff --git a/web/src/features/accounts/components/account-detail.tsx b/web/src/features/accounts/components/account-detail.tsx index fa9ad89..94d599d 100644 --- a/web/src/features/accounts/components/account-detail.tsx +++ b/web/src/features/accounts/components/account-detail.tsx @@ -4,7 +4,7 @@ * Unauthorized use or distribution is prohibited. */ -import { AccountEntity } from '../data/schema' +import { AccountEntity, MailerType } from '../data/schema' import { ScrollArea } from '@/components/ui/scroll-area' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -62,13 +62,21 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { <span className="text-muted-foreground">Enabled:</span> <Checkbox checked={currentRow.enabled} disabled /> </div> + <div className="flex flex-wrap items-center gap-2"> + <span className="text-muted-foreground">Mailer Type:</span> + <span>{currentRow.mailer_type}</span> + </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Minimal Sync:</span> - <Checkbox checked={currentRow.minimal_sync} disabled /> + {currentRow.minimal_sync !== undefined ? ( + <Checkbox checked={currentRow.minimal_sync} disabled /> + ) : ( + <span className="text-muted-foreground">n/a</span> + )} </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Full Sync Interval:</span> - <span>every {currentRow.full_sync_interval_min} min</span> + <span>{currentRow.full_sync_interval_min ? `every ${currentRow.full_sync_interval_min} min` : "n/a"}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Incremental Sync Interval:</span> @@ -77,7 +85,7 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { <div className="flex flex-col gap-2"> <span className="text-muted-foreground">Capabilities:</span> <code className="rounded-md bg-muted/50 px-2 py-1 text-sm border overflow-x-auto inline-block"> - {currentRow.capabilities.join(', ')} + {currentRow.capabilities ? currentRow.capabilities.join(', ') : "n/a"} </code> </div> <div className="flex flex-wrap items-center gap-2"> @@ -97,7 +105,7 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { {/* Server Configurations Tab */} <TabsContent value="server"> - <div className="grid gap-4 mt-4 md:grid-cols-2"> + {currentRow.mailer_type === MailerType.ImapSmtp ? <div className="grid gap-4 mt-4 md:grid-cols-2"> {/* IMAP Configuration Card */} <Card> <CardHeader> @@ -107,19 +115,19 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { <div className="flex flex-col gap-2"> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Host:</span> - <span>{currentRow.imap.host}</span> + <span>{currentRow.imap?.host}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Port:</span> - <span>{currentRow.imap.port}</span> + <span>{currentRow.imap?.port}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Encryption:</span> - <span>{currentRow.imap.encryption}</span> + <span>{currentRow.imap?.encryption}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Auth:</span> - {currentRow.imap.auth.auth_type === 'OAuth2' ? ( + {currentRow.imap?.auth.auth_type === 'OAuth2' ? ( <Badge variant="outline" className="bg-blue-100 text-blue-800"> OAuth2 </Badge> @@ -131,7 +139,7 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Use Proxy:</span> - <span>{currentRow.imap.use_proxy ? "true" : "false"}</span> + <span>{currentRow.imap?.use_proxy ? "true" : "false"}</span> </div> </div> </CardContent> @@ -146,19 +154,19 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { <div className="flex flex-col gap-2"> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Host:</span> - <span>{currentRow.smtp.host}</span> + <span>{currentRow.smtp?.host}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Port:</span> - <span>{currentRow.smtp.port}</span> + <span>{currentRow.smtp?.port}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Encryption:</span> - <span>{currentRow.smtp.encryption}</span> + <span>{currentRow.smtp?.encryption}</span> </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Auth:</span> - {currentRow.smtp.auth.auth_type === 'OAuth2' ? ( + {currentRow.smtp?.auth.auth_type === 'OAuth2' ? ( <Badge variant="outline" className="bg-blue-100 text-blue-800"> OAuth2 </Badge> @@ -170,12 +178,14 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { </div> <div className="flex flex-wrap items-center gap-2"> <span className="text-muted-foreground">Use Proxy:</span> - <span>{currentRow.smtp.use_proxy ? "true" : "false"}</span> + <span>{currentRow.smtp?.use_proxy ? "true" : "false"}</span> </div> </div> </CardContent> </Card> - </div> + </div> : <div className="mt-4 text-muted-foreground"> + No IMAP/SMTP configuration required (using {currentRow.mailer_type}). + </div>} </TabsContent> {/* Sync Folders Tab */} diff --git a/web/src/features/accounts/components/action-dialog.tsx b/web/src/features/accounts/components/action-dialog.tsx index 8cd0771..c4951fe 100644 --- a/web/src/features/accounts/components/action-dialog.tsx +++ b/web/src/features/accounts/components/action-dialog.tsx @@ -10,7 +10,7 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Form } from '@/components/ui/form'; -import { AccountEntity } from '../data/schema'; +import { AccountEntity, ImapConfig, SmtpConfig } from '../data/schema'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useToast } from '@/hooks/use-toast'; @@ -212,9 +212,26 @@ const defaultValues: Account = { incremental_sync_interval_sec: 30, }; +const emptyImap: ImapConfig = { + host: "", + port: 0, + encryption: "None", + auth: { auth_type: "Password", password: undefined }, + use_proxy: undefined, +}; + +const emptySmtp: SmtpConfig = { + host: "", + port: 0, + encryption: "None", + auth: { auth_type: "Password", password: undefined }, + use_proxy: undefined, +}; + + const mapCurrentRowToFormValues = (currentRow: AccountEntity): Account => { - const imap = { ...currentRow.imap }; - const smtp = { ...currentRow.smtp }; + const imap = { ...(currentRow.imap ?? emptyImap) }; + const smtp = { ...(currentRow.smtp ?? emptySmtp) }; // Handle password and use_proxy conversion imap.auth = { ...imap.auth, password: undefined }; @@ -233,9 +250,9 @@ const mapCurrentRowToFormValues = (currentRow: AccountEntity): Account => { imap, smtp, enabled: currentRow.enabled, - minimal_sync: currentRow.minimal_sync, + minimal_sync: currentRow.minimal_sync ?? false, date_since: currentRow.date_since ?? undefined, - full_sync_interval_min: currentRow.full_sync_interval_min, + full_sync_interval_min: currentRow.full_sync_interval_min ?? 60, incremental_sync_interval_sec: currentRow.incremental_sync_interval_sec, }; diff --git a/web/src/features/accounts/components/oauth2-action.tsx b/web/src/features/accounts/components/oauth2-action.tsx index 2cf8ece..b6b67f0 100644 --- a/web/src/features/accounts/components/oauth2-action.tsx +++ b/web/src/features/accounts/components/oauth2-action.tsx @@ -7,7 +7,7 @@ import { Row } from '@tanstack/react-table' import { Button } from '@/components/ui/button' import { useAccountContext } from '../context' -import { AccountEntity } from '../data/schema' +import { AccountEntity, MailerType } from '../data/schema' interface DataTableRowActionsProps { row: Row<AccountEntity> @@ -16,12 +16,16 @@ interface DataTableRowActionsProps { export function OAuth2Action({ row }: DataTableRowActionsProps) { const { setOpen, setCurrentRow } = useAccountContext() - return row.original.imap.auth.auth_type === 'OAuth2' ? ( + return ( + (row.original.mailer_type === MailerType.ImapSmtp && + row.original.imap?.auth.auth_type === 'OAuth2') || + row.original.mailer_type === MailerType.GmailApi + ) ? ( <Button variant='ghost' onClick={() => { setCurrentRow(row.original) setOpen('oauth2') - }}><span className="text-xs text-blue-500 cursor-pointer underline hover:text-blue-700">{row.original.imap.auth.auth_type}</span></Button> + }}><span className="text-xs text-blue-500 cursor-pointer underline hover:text-blue-700">OAuth2</span></Button> ) : ( - <span className="text-xs cursor-pointer">{row.original.imap.auth.auth_type}</span> + <span className="text-xs cursor-pointer">Password</span> ) } diff --git a/web/src/features/accounts/data/schema.ts b/web/src/features/accounts/data/schema.ts index f33e05c..518b11a 100644 --- a/web/src/features/accounts/data/schema.ts +++ b/web/src/features/accounts/data/schema.ts @@ -14,7 +14,7 @@ interface AuthConfig { password?: string; } -interface SmtpConfig { +export interface SmtpConfig { host: string; port: number; // integer, 0-65535 encryption: Encryption; @@ -22,7 +22,7 @@ interface SmtpConfig { use_proxy?: number; } -interface ImapConfig { +export interface ImapConfig { host: string; port: number; // integer, 0-65535 encryption: Encryption; @@ -42,18 +42,28 @@ interface DateSelection { export interface AccountEntity { id: number; - imap: ImapConfig; - smtp: SmtpConfig; + imap?: ImapConfig; + smtp?: SmtpConfig; enabled: boolean; + mailer_type: MailerType, deleted: boolean; name?: string, email: string; - minimal_sync: boolean; - capabilities: string[]; + minimal_sync?: boolean; + capabilities?: string[]; date_since?: DateSelection; sync_folders?: string[]; - full_sync_interval_min: number; + full_sync_interval_min?: number; incremental_sync_interval_sec: number; created_at: number; updated_at: number; +} + + +// Represents the method used to access/manage emails +export enum MailerType { + /** Use IMAP/SMTP protocol */ + ImapSmtp = "ImapSmtp", + /** Use Gmail API */ + GmailApi = "GmailApi", } \ No newline at end of file diff --git a/web/src/features/accounts/index.tsx b/web/src/features/accounts/index.tsx index a4831d8..bc0de6a 100644 --- a/web/src/features/accounts/index.tsx +++ b/web/src/features/accounts/index.tsx @@ -17,7 +17,7 @@ import AccountProvider, { } from './context' import { Plus } from 'lucide-react' import Logo from '@/assets/logo.svg' -import { AccountEntity } from './data/schema' +import { AccountEntity, MailerType } from './data/schema' import { AccountDetailDrawer } from './components/account-detail' import { list_accounts } from '@/api/account/api' import { TableSkeleton } from '@/components/table-skeleton' @@ -141,10 +141,14 @@ export default function Accounts() { onOpenChange={() => setOpen('detail')} currentRow={currentRow} /> - {currentRow.imap.auth.auth_type === 'OAuth2' && <OAuth2TokensDialog open={open === 'oauth2'} + {( + (currentRow.mailer_type === MailerType.ImapSmtp && + currentRow.imap?.auth.auth_type === 'OAuth2') || + currentRow.mailer_type === MailerType.GmailApi + ) && <OAuth2TokensDialog open={open === 'oauth2'} onOpenChange={() => setOpen('oauth2')} currentRow={currentRow} - />} + />} </> )} </AccountProvider>