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

- Introduced `MailerType` enum in Account model
- Enables selection of different mailer backends (e.g., Gmail API)
This commit is contained in:
rustmailer
2025-08-28 03:20:49 +08:00
parent a9ef330418
commit fd64acc24f
65 changed files with 971 additions and 639 deletions
+19 -7
View File
@@ -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.
+12 -330
View File
@@ -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<Self> {
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<Account> {
let account_entity =
secondary_find_impl::<Account>(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<Account> {
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<Option<Account>> {
secondary_find_impl::<Account>(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<Account> {
// 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::<Account>(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::<Account>(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<String>,
) -> RustMailerResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<Account>(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<String>,
) -> RustMailerResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<Account>(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<String>,
) -> RustMailerResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<Account>(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::<Account>(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<Vec<Account>> {
list_all_impl(DB_MANAGER.meta_db()).await
}
pub async fn minimal_list() -> RustMailerResult<Vec<MinimalAccount>> {
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::<Vec<MinimalAccount>>();
Ok(result)
}
pub async fn count() -> RustMailerResult<usize> {
count_by_unique_secondary_key_impl::<Account>(DB_MANAGER.meta_db(), AccountKey::id).await
}
pub async fn paginate_list(
page: Option<u64>,
page_size: Option<u64>,
desc: Option<bool>,
) -> RustMailerResult<DataPage<Account>> {
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<Account> {
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<bool> for Encryption {
}
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Enum)]
pub enum MailerType {
/// Use IMAP/SMTP protocol
#[default]
ImapSmtp,
/// Use Gmail API
GmailApi,
}
+1
View File
@@ -7,3 +7,4 @@ pub mod entity;
pub mod payload;
pub mod since;
pub mod status;
pub mod v2;
+35 -7
View File
@@ -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<String>,
/// IMAP server configuration
pub imap: ImapConfig,
pub imap: Option<ImapConfig>,
/// SMTP server configuration
pub smtp: SmtpConfig,
pub smtp: Option<SmtpConfig>,
/// 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<bool>,
/// Full sync interval (minutes), default 30m
#[oai(validator(minimum(value = "10"), maximum(value = "10080")))]
pub full_sync_interval_min: Option<i64>,
@@ -60,13 +63,38 @@ pub struct AccountCreateRequest {
}
impl AccountCreateRequest {
pub fn create_entity(self) -> RustMailerResult<Account> {
pub fn create_entity(self) -> RustMailerResult<AccountV2> {
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<()> {
+474
View File
@@ -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<ImapConfig>,
/// SMTP server configuration
pub smtp: Option<SmtpConfig>,
/// 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<String>,
/// 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<bool>,
/// IMAP Server-supported capability flags
pub capabilities: Option<Vec<String>>,
/// DSN (Delivery Status Notification) support flag
pub dsn_capable: Option<bool>,
/// 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<DateSince>,
/// 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<String>,
/// Full sync interval (minutes), default 30m
pub full_sync_interval_min: Option<i64>,
/// 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<String>,
/// 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<Self> {
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<AccountV2> {
let account_entity =
secondary_find_impl::<AccountV2>(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<AccountV2> {
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<Option<AccountV2>> {
secondary_find_impl::<AccountV2>(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<AccountV2> {
// 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::<AccountV2>(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::<AccountV2>(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<String>,
) -> RustMailerResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<AccountV2>(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<String>,
) -> RustMailerResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<AccountV2>(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<String>,
) -> RustMailerResult<()> {
update_impl(DB_MANAGER.meta_db(), move |rw| {
rw.get().secondary::<AccountV2>(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::<AccountV2>(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<Vec<AccountV2>> {
list_all_impl(DB_MANAGER.meta_db()).await
}
pub async fn minimal_list() -> RustMailerResult<Vec<MinimalAccount>> {
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::<Vec<MinimalAccount>>();
Ok(result)
}
pub async fn count() -> RustMailerResult<usize> {
count_by_unique_secondary_key_impl::<AccountV2>(DB_MANAGER.meta_db(), AccountV2Key::id)
.await
}
pub async fn paginate_list(
page: Option<u64>,
page_size: Option<u64>,
desc: Option<bool>,
) -> RustMailerResult<DataPage<AccountV2>> {
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<AccountV2> {
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<AccountV2> 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<Account> 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,
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ use tracing::info;
use crate::{
id,
modules::{
cache::imap::envelope_v2::EmailEnvelopeV2,
cache::imap::v2::EmailEnvelopeV2,
database::{batch_delete_impl, filter_by_secondary_key_impl, manager::DB_MANAGER},
error::{code::ErrorCode, RustMailerResult},
utils::envelope_hash,
+6 -6
View File
@@ -9,13 +9,13 @@ use std::collections::HashSet;
use std::sync::LazyLock;
use tracing::warn;
use crate::modules::account::entity::Account;
use crate::modules::account::v2::AccountV2;
use crate::modules::cache::imap::address::AddressEntity;
use crate::modules::cache::imap::envelope_v2::EmailEnvelopeV2;
use crate::modules::cache::imap::flags_to_hash;
use crate::modules::cache::imap::mailbox::EnvelopeFlag;
use crate::modules::cache::imap::minimal::MinimalEnvelope;
use crate::modules::cache::imap::thread::EmailThread;
use crate::modules::cache::imap::v2::EmailEnvelopeV2;
use crate::modules::context::Initialize;
use crate::modules::error::RustMailerResult;
use crate::modules::hook::channel::{Event, EVENT_CHANNEL};
@@ -36,7 +36,7 @@ pub struct EnvelopeFlagsManager;
impl EnvelopeFlagsManager {
pub async fn load_state() -> RustMailerResult<()> {
let all_accounts = Account::list_all().await?;
let all_accounts = AccountV2::list_all().await?;
stream::iter(all_accounts)
.filter(|account| futures::future::ready(account.enabled))
@@ -132,13 +132,13 @@ impl EnvelopeFlagsManager {
}
pub async fn update_envelope_flags(
account: &Account,
account: &AccountV2,
mailbox_id: u64,
data: Vec<(u32, Vec<EnvelopeFlag>)>,
) -> RustMailerResult<()> {
RUSTMAILER_MAIL_FLAG_CHANGE_TOTAL.inc_by(data.len() as u64);
for (uid, flags) in data {
if !account.minimal_sync
if !account.minimal_sync()
&& EventHookTask::event_watched(account.id, EventType::EmailFlagsChanged).await?
{
if let Some(current) = EmailEnvelopeV2::find(account.id, mailbox_id, uid).await? {
@@ -170,7 +170,7 @@ impl EnvelopeFlagsManager {
}
let flags_hash = flags_to_hash(&flags);
if !account.minimal_sync {
if !account.minimal_sync() {
EmailEnvelopeV2::update_flags(account.id, mailbox_id, uid, &flags, flags_hash)
.await?;
}
+1 -1
View File
@@ -11,7 +11,7 @@ use tracing::{error, info};
use crate::{
modules::{
cache::imap::{envelope_v2::EmailEnvelopeV2, manager::EnvelopeFlagsManager},
cache::imap::{v2::EmailEnvelopeV2, manager::EnvelopeFlagsManager},
database::{
batch_delete_impl, batch_insert_impl, delete_impl, filter_by_secondary_key_impl,
manager::DB_MANAGER, update_impl,
+3 -3
View File
@@ -8,8 +8,8 @@ use crate::{
calculate_hash,
modules::{
cache::imap::{
address::AddressEntity, envelope::EmailEnvelope, envelope_v2::EmailEnvelopeV2,
minimal::MinimalEnvelope, thread::EmailThread,
address::AddressEntity, envelope::EmailEnvelope, minimal::MinimalEnvelope,
thread::EmailThread, v2::EmailEnvelopeV2,
},
database::ModelsAdapter,
},
@@ -19,7 +19,6 @@ use mailbox::{EmailFlag, EnvelopeFlag, MailBox};
use native_db::Models;
pub mod address;
pub mod envelope;
pub mod envelope_v2;
pub mod mailbox;
pub mod manager;
pub mod minimal;
@@ -28,6 +27,7 @@ pub mod task;
#[cfg(test)]
mod tests;
pub mod thread;
pub mod v2;
pub static ENVELOPE_MODELS: LazyLock<Models> = LazyLock::new(|| {
let mut adapter = ModelsAdapter::new();
+22 -23
View File
@@ -4,12 +4,10 @@
use crate::{
modules::{
account::{entity::Account, since::DateSince, status::AccountRunningState},
account::{since::DateSince, status::AccountRunningState, v2::AccountV2},
bounce::parser::{extract_bounce_report, BounceReport},
cache::imap::{
diff,
envelope_v2::EmailEnvelopeV2,
find_deleted_mailboxes, find_flag_updates, find_intersecting_mailboxes,
diff, find_deleted_mailboxes, find_flag_updates, find_intersecting_mailboxes,
find_missing_mailboxes, find_missing_remote_uids,
mailbox::{EnvelopeFlag, MailBox},
manager::EnvelopeFlagsManager,
@@ -18,6 +16,7 @@ use crate::{
rebuild::{rebuild_mailbox_cache, rebuild_mailbox_cache_since_date},
sync_type::SyncType,
},
v2::EmailEnvelopeV2,
},
common::AddrVec,
context::executors::RUST_MAIL_CONTEXT,
@@ -138,7 +137,7 @@ pub async fn fetch_and_save_since_date(
}
pub async fn fetch_and_save_full_mailbox(
account: &Account,
account: &AccountV2,
mailbox: &MailBox,
total: u32,
initial: bool,
@@ -148,7 +147,7 @@ pub async fn fetch_and_save_full_mailbox(
let mut inserted_count = 0;
let account_id = account.id;
let minimal_sync = account.minimal_sync;
let minimal_sync = account.minimal_sync();
if initial {
AccountRunningState::set_initial_current_syncing_folder(
@@ -298,7 +297,7 @@ pub fn compress_uid_list(nums: Vec<u32>) -> String {
}
pub async fn compare_and_sync_mailbox(
account: &Account,
account: &AccountV2,
remote_mailboxes: &[MailBox],
local_mailboxes: &[MailBox],
sync_type: &SyncType,
@@ -407,7 +406,7 @@ pub async fn compare_and_sync_mailbox(
}
async fn cleanup_deleted_mailboxes(
account: &Account,
account: &AccountV2,
deleted_mailboxes: &[MailBox],
) -> RustMailerResult<()> {
let start_time = Instant::now();
@@ -424,7 +423,7 @@ async fn cleanup_deleted_mailboxes(
}
/// Sync recent 200 envelopes's flags
async fn sync_recent_envelope_flags(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
uid_next: u32,
@@ -457,7 +456,7 @@ async fn sync_recent_envelope_flags(
//only check new emails and sync
async fn perform_incremental_sync(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
sync_count: usize,
@@ -491,10 +490,10 @@ async fn perform_incremental_sync(
.fetch_uid_list(
max_uid + 1,
&remote_mailbox.encoded_name(),
account.minimal_sync,
account.minimal_sync(),
)
.await?;
let uid_list = parse_fetch_metadata(fetches, !account.minimal_sync)?;
let uid_list = parse_fetch_metadata(fetches, !account.minimal_sync())?;
// This is just a precaution in case the server's behavior deviates from expectations.
let uid_list = uid_list
@@ -522,7 +521,7 @@ async fn perform_incremental_sync(
date_since.since_date()?.as_str(),
remote_mailbox,
false,
account.minimal_sync,
account.minimal_sync(),
)
.await?;
}
@@ -544,7 +543,7 @@ async fn perform_incremental_sync(
}
//
async fn perform_full_sync(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
) -> RustMailerResult<()> {
@@ -666,7 +665,7 @@ async fn diff_uids_and_flags(
}
async fn cleanup_missing_remote_emails(
account: &Account,
account: &AccountV2,
mailbox_id: u64,
mailbox_name: &str,
local_uid_flags_index: &AHashMap<u32, u64>,
@@ -687,7 +686,7 @@ async fn cleanup_missing_remote_emails(
}
pub async fn fetch_and_store_new_envelopes_by_uid_list(
account: &Account,
account: &AccountV2,
local_mailbox_id: u64,
remote: &MailBox,
uid_list: Vec<(u32, u64)>,
@@ -718,7 +717,7 @@ pub async fn fetch_and_store_new_envelopes_by_uid_list(
info!("Account {}: Mailbox '{}' has {} new message UID(s) to fetch metadata. Starting download...", account.id, &remote.name, len);
// Process minimal sync case first
if account.minimal_sync {
if account.minimal_sync() {
let envelopes: Vec<MinimalEnvelope> = uid_list
.clone()
.into_iter()
@@ -768,7 +767,7 @@ pub async fn fetch_and_store_new_envelopes_by_uid_list(
}
async fn process_email_added_events(
account: &Account,
account: &AccountV2,
remote: &MailBox,
fetches: &[Fetch],
) -> RustMailerResult<()> {
@@ -834,13 +833,13 @@ async fn process_email_added_events(
}
async fn handle_minimal_sync_or_metadata_fetch(
account: &Account,
account: &AccountV2,
local_mailbox_id: u64,
remote: &MailBox,
uid_list: Vec<(u32, u64)>,
len: usize,
) -> RustMailerResult<()> {
if account.minimal_sync {
if account.minimal_sync() {
let envelopes: Vec<MinimalEnvelope> = uid_list
.into_iter()
.map(|(uid, flags_hash)| MinimalEnvelope {
@@ -877,7 +876,7 @@ async fn handle_minimal_sync_or_metadata_fetch(
}
async fn process_bounce_reports(
account: &Account,
account: &AccountV2,
remote: &MailBox,
fetches: &[Fetch],
) -> RustMailerResult<()> {
@@ -925,7 +924,7 @@ async fn process_bounce_reports(
}
async fn submit_bounce_event(
account: &Account,
account: &AccountV2,
remote: &MailBox,
uid: u32,
fetch: &Fetch,
@@ -960,7 +959,7 @@ async fn submit_bounce_event(
}
async fn submit_feedback_report_event(
account: &Account,
account: &AccountV2,
remote: &MailBox,
uid: u32,
fetch: &Fetch,
+2 -2
View File
@@ -3,7 +3,7 @@
// Unauthorized copying, modification, or distribution is prohibited.
use crate::modules::{
account::{entity::Account, status::AccountRunningState},
account::{status::AccountRunningState, v2::AccountV2},
cache::imap::{mailbox::MailBox, manager::EnvelopeFlagsManager},
error::RustMailerResult,
hook::{
@@ -29,7 +29,7 @@ pub mod sync_type;
static SYNC_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub async fn execute_account_sync(account: &Account) -> RustMailerResult<()> {
pub async fn execute_account_sync(account: &AccountV2) -> RustMailerResult<()> {
let start_time = Instant::now();
let account_id = account.id;
+8 -16
View File
@@ -3,7 +3,7 @@
// Unauthorized copying, modification, or distribution is prohibited.
use crate::modules::{
account::{entity::Account, since::DateSince},
account::{since::DateSince, v2::AccountV2},
cache::imap::{
mailbox::MailBox,
manager::EnvelopeFlagsManager,
@@ -15,7 +15,7 @@ use std::time::Instant;
use tracing::{error, info, warn};
pub async fn rebuild_cache(
account: &Account,
account: &AccountV2,
remote_mailboxes: &[MailBox],
) -> RustMailerResult<()> {
let start_time = Instant::now();
@@ -62,7 +62,7 @@ pub async fn rebuild_cache(
}
pub async fn rebuild_cache_since_date(
account: &Account,
account: &AccountV2,
remote_mailboxes: &[MailBox],
date_since: &DateSince,
) -> RustMailerResult<()> {
@@ -80,21 +80,13 @@ pub async fn rebuild_cache_since_date(
);
continue;
}
// total_inserted += fetch_and_save_since_date(
// account.id,
// date.as_str(),
// mailbox,
// true,
// account.minimal_sync,
// )
// .await?;
match fetch_and_save_since_date(
account.id,
date.as_str(),
mailbox,
true,
account.minimal_sync,
account.minimal_sync(),
)
.await
{
@@ -126,7 +118,7 @@ pub async fn rebuild_cache_since_date(
}
pub async fn should_rebuild_cache(
account: &Account,
account: &AccountV2,
mailbox_count: usize,
local_envelope_count: usize,
) -> RustMailerResult<bool> {
@@ -147,7 +139,7 @@ pub async fn should_rebuild_cache(
}
pub async fn rebuild_mailbox_cache(
account: &Account,
account: &AccountV2,
local_mailbox: &MailBox,
remote_mailbox: &MailBox,
) -> RustMailerResult<()> {
@@ -171,7 +163,7 @@ pub async fn rebuild_mailbox_cache(
}
pub async fn rebuild_mailbox_cache_since_date(
account: &Account,
account: &AccountV2,
local_mailbox_id: u64,
date_since: &DateSince,
remote: &MailBox,
@@ -191,7 +183,7 @@ pub async fn rebuild_mailbox_cache_since_date(
date_since.since_date()?.as_str(),
remote,
false,
account.minimal_sync,
account.minimal_sync(),
)
.await?;
info!(
+8 -8
View File
@@ -7,7 +7,7 @@ use std::collections::BTreeSet;
use crate::{
decode_mailbox_name,
modules::{
account::entity::Account,
account::v2::AccountV2,
cache::imap::mailbox::{AttributeEnum, MailBox},
context::executors::RUST_MAIL_CONTEXT,
error::{code::ErrorCode, RustMailerResult},
@@ -26,7 +26,7 @@ use crate::{
use async_imap::types::Name;
use tracing::{info, warn};
pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox>> {
pub async fn get_sync_folders(account: &AccountV2) -> RustMailerResult<Vec<MailBox>> {
let executor = RUST_MAIL_CONTEXT.imap(account.id).await?;
let names = executor.list_all_mailboxes().await?;
if names.is_empty() {
@@ -45,7 +45,7 @@ pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox
mailboxes.iter().map(|(m, _)| m.name.clone()).collect(),
)
.await?;
let account = Account::get(account.id).await?;
let account = AccountV2::get(account.id).await?;
let subscribed = &account.sync_folders;
let is_noselect = |mailbox: &MailBox| {
mailbox
@@ -83,7 +83,7 @@ pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox
.iter()
.map(|n| decode_mailbox_name!(n.name().to_string()))
.collect();
Account::update_sync_folders(account.id, sync_folders).await?;
AccountV2::update_sync_folders(account.id, sync_folders).await?;
} else {
warn!(
"Account {}: No subscribed mailboxes found. This is unexpected — IMAP server should at least provide INBOX.",
@@ -99,12 +99,12 @@ pub async fn get_sync_folders(account: &Account) -> RustMailerResult<Vec<MailBox
}
pub async fn detect_mailbox_changes(
account: &Account,
account: &AccountV2,
all_names: BTreeSet<String>,
) -> RustMailerResult<()> {
if account.known_folders.is_empty() {
// First time sync: just save without comparing
Account::update_known_folders(account.id, all_names).await?;
AccountV2::update_known_folders(account.id, all_names).await?;
return Ok(());
}
@@ -136,7 +136,7 @@ pub async fn detect_mailbox_changes(
// Note: When all subscribed folders are deleted (remaining_sync_folders empty),
// the system's default behavior is to automatically fall back to syncing
// only the default folders (INBOX and Sent) in subsequent operations
Account::update_sync_folders(account.id, remaining_sync_folders).await?;
AccountV2::update_sync_folders(account.id, remaining_sync_folders).await?;
}
info!(
@@ -187,7 +187,7 @@ pub async fn detect_mailbox_changes(
// Update known folders only if there were changes
if has_changes {
Account::update_known_folders(account.id, all_names).await?;
AccountV2::update_known_folders(account.id, all_names).await?;
}
Ok(())
}
+8 -3
View File
@@ -4,12 +4,15 @@
use crate::{
modules::{
account::{entity::Account, status::AccountRunningState},
account::{status::AccountRunningState, v2::AccountV2},
error::RustMailerResult,
},
utc_now,
};
/// Default interval (in minutes) for full synchronization.
pub const DEFAULT_FULL_SYNC_INTERVAL_MIN: i64 = 30;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SyncType {
/// Full synchronization, typically used for the first sync or after major changes.
@@ -20,14 +23,16 @@ pub enum SyncType {
SkipSync,
}
pub async fn determine_sync_type(account: &Account) -> RustMailerResult<SyncType> {
pub async fn determine_sync_type(account: &AccountV2) -> RustMailerResult<SyncType> {
Ok(match AccountRunningState::get(account.id).await? {
Some(info) => {
let now = utc_now!();
if is_time_for_full_sync(
now,
info.last_full_sync_start,
account.full_sync_interval_min,
account
.full_sync_interval_min
.unwrap_or(DEFAULT_FULL_SYNC_INTERVAL_MIN),
) {
AccountRunningState::set_full_sync_start(account.id).await?;
SyncType::FullSync
+11 -3
View File
@@ -7,7 +7,7 @@ use crate::modules::cache::imap::sync::execute_account_sync;
use crate::modules::oauth2::token::OAuth2AccessToken;
use crate::modules::scheduler::periodic::TaskHandle;
use crate::modules::{
account::{dispatcher::STATUS_DISPATCHER, entity::Account},
account::{dispatcher::STATUS_DISPATCHER, v2::AccountV2},
error::RustMailerResult,
scheduler::periodic::PeriodicTask,
};
@@ -40,7 +40,7 @@ impl AccountSyncTask {
let task = move |param: Option<u64>| {
let account_id = param.unwrap();
Box::pin(async move {
let account = Account::get(account_id).await.ok();
let account = AccountV2::get(account_id).await.ok();
match account {
Some(account) => {
if !account.enabled {
@@ -54,7 +54,15 @@ impl AccountSyncTask {
);
}
} else {
if let AuthType::OAuth2 = account.imap.auth.auth_type {
if let AuthType::OAuth2 = account
.imap
.as_ref()
.expect(
"BUG: account.imap is None, but this should never happen here",
)
.auth
.auth_type
{
if OAuth2AccessToken::get(account.id).await?.is_none() {
if utc_now!() % 300_000 == 0 {
warn!("Account {}: Sync aborted. OAuth2 authorization not completed. Please visit the rustmailer admin page to authorize this account.", account_id);
+1 -1
View File
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::{
modules::{
cache::imap::envelope_v2::EmailEnvelopeV2,
cache::imap::v2::EmailEnvelopeV2,
database::{
batch_delete_impl, delete_impl, manager::DB_MANAGER, paginate_secondary_scan_impl,
},
+3 -3
View File
@@ -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<Account> = accounts.into_iter().filter(|a| a.enabled).collect();
let accounts = AccountV2::list_all().await?;
let active_accounts: Vec<AccountV2> = accounts.into_iter().filter(|a| a.enabled).collect();
if active_accounts.is_empty() {
info!("No active accounts found for IMAP initialization.");
+13 -3
View File
@@ -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<DatabaseManager> = 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::<AccountV2>()
.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);
+2
View File
@@ -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::<License>();
self.register_model::<CachedMailSettings>();
self.register_model::<Account>();
self.register_model::<AccountV2>();
self.register_model::<EmailTemplate>();
self.register_model::<Mta>();
self.register_model::<OAuth2>();
+6 -6
View File
@@ -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<Account> = r_transaction
let entities: Vec<AccountV2> = r_transaction
.scan()
.secondary(AccountKey::id)
.unwrap()
@@ -51,7 +51,7 @@ fn test3() {
.unwrap();
println!("{:#?}", entities);
let entities: Vec<Account> = r_transaction
let entities: Vec<AccountV2> = 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);
}
+1 -1
View File
@@ -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;
+42 -25
View File
@@ -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<DateSince> for rustmailer_grpc::DateSince {
}
}
impl TryFrom<rustmailer_grpc::Account> for Account {
impl TryFrom<rustmailer_grpc::Account> for AccountV2 {
type Error = &'static str;
fn try_from(value: rustmailer_grpc::Account) -> Result<Self, Self::Error> {
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<rustmailer_grpc::Account> for Account {
}
}
impl From<Account> for rustmailer_grpc::Account {
fn from(value: Account) -> Self {
impl From<AccountV2> 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<rustmailer_grpc::AccountCreateRequest> 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<MinimalAccount> for rustmailer_grpc::MinimalAccount {
}
}
}
impl TryFrom<i32> for MailerType {
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(MailerType::ImapSmtp),
1 => Ok(MailerType::GmailApi),
_ => Err("Invalid value for Unit"),
}
}
}
impl From<MailerType> for i32 {
fn from(value: MailerType) -> Self {
match value {
MailerType::ImapSmtp => 0,
MailerType::GmailApi => 1,
}
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -5,7 +5,7 @@
use crate::modules::{
cache::imap::{
envelope::Received,
envelope_v2::EmailEnvelopeV2,
v2::EmailEnvelopeV2,
mailbox::{EmailFlag, EnvelopeFlag},
},
common::Addr,
+4 -4
View File
@@ -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<Self> {
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
+1 -2
View File
@@ -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(),
+23 -26
View File
@@ -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<Account> {
pub async fn fetch_account(&self) -> RustMailerResult<AccountV2> {
// 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> {
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<Client> {
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<Session<Box<dyn SessionStream>>> {
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<String> = 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,
+2 -2
View File
@@ -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!(
+2 -2
View File
@@ -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())
+2 -2
View File
@@ -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
}
+4 -4
View File
@@ -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<Vec<MailBox>> {
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<Vec<MailBox>> {
Account::check_account_active(account_id).await?;
AccountV2::check_account_active(account_id).await?;
request_imap_subscribed_mailbox_list(account_id).await
}
+11 -5
View File
@@ -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
}
+3 -3
View File
@@ -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())
+2 -2
View File
@@ -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())),
+2 -2
View File
@@ -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<cacache::Reader> {
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!(
+2 -2
View File
@@ -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<MessageContent> {
Account::check_account_active(account_id).await?;
AccountV2::check_account_active(account_id).await?;
let mut plain: Option<PlainText> = None;
let mut html: Option<String> = None;
+2 -2
View File
@@ -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());
+2 -2
View File
@@ -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?;
+2 -2
View File
@@ -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?;
+2 -2
View File
@@ -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!(
+9 -9
View File
@@ -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. \
+2 -2
View File
@@ -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());
+4 -4
View File
@@ -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,
+2 -2
View File
@@ -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();
+12 -12
View File
@@ -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,
+1 -1
View File
@@ -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};
+2 -2
View File
@@ -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,
+20 -16
View File
@@ -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)
+5 -5
View File
@@ -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 {
+9 -9
View File
@@ -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>>,
+4 -4
View File
@@ -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?;
+5 -5
View File
@@ -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 {
+4 -4
View File
@@ -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
};
+4 -6
View File
@@ -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(())
}
+2 -2
View File
@@ -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)?;
+3 -3
View File
@@ -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,
+3 -3
View File
@@ -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);
}
}
+9
View File
@@ -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;
+44
View File
@@ -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());
}
}
@@ -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 */}
@@ -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,
};
@@ -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>
)
}
+17 -7
View File
@@ -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",
}
+7 -3
View File
@@ -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>