feat: support export account emails to a single mbox file

This commit is contained in:
rustmailer
2026-04-25 05:39:58 +08:00
parent 0b866c81ff
commit fa70437a62
22 changed files with 610 additions and 69 deletions
+4 -3
View File
@@ -19,8 +19,9 @@
pub mod entity;
pub mod grant;
pub mod migration;
pub mod payload;
pub mod state;
pub mod since;
pub mod old_state;
pub mod payload;
pub mod since;
pub mod state;
pub mod stats;
pub mod view;
+8
View File
@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "web-api", derive(poem_openapi::Object))]
pub struct AccountStats {
pub total_size: u64,
pub total_count: u64,
}
+14 -11
View File
@@ -21,12 +21,15 @@ use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::{
common::paginated::DataPage, error::{BichonResult, code::ErrorCode}, raise_error, store::{
envelope::Envelope,
tantivy::{
attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER, model::AttachmentModel,
},
}
common::paginated::DataPage,
error::{code::ErrorCode, BichonResult},
raise_error,
store::{
envelope::Envelope,
tantivy::{
attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER, model::AttachmentModel,
},
},
};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
@@ -66,11 +69,11 @@ pub enum SortBy {
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "web-api", derive(poem_openapi::Object))]
pub struct EmailSearchRequest {
filter: EmailSearchFilter,
page: u64,
page_size: u64,
sort_by: Option<SortBy>,
desc: Option<bool>,
pub filter: EmailSearchFilter,
pub page: u64,
pub page_size: u64,
pub sort_by: Option<SortBy>,
pub desc: Option<bool>,
}
impl EmailSearchRequest {
pub fn validate(&self) -> BichonResult<()> {
+72 -19
View File
@@ -25,22 +25,30 @@ use std::{
};
use crate::{
account::migration::AccountModel, common::{paginated::DataPage, signal::SIGNAL_MANAGER}, dashboard::{DashboardStats, Group, LargestEmail, TimeBucket}, error::{BichonResult, code::ErrorCode}, message::{
search::{EmailSearchFilter, SortBy},
tags::{TagAction, TagCount, TagsRequest},
}, raise_error, settings::dir::DATA_DIR_MANAGER, store::{
envelope::Envelope,
storage::BLOB_MANAGER,
tantivy::{
fatal_commit,
fields::{
F_ACCOUNT_ID, F_DATE, F_FROM, F_REGULAR_ATTACHMENT_COUNT, F_SIZE, F_TAGS,
F_THREAD_ID, F_UID,
},
model::{EnvelopeWithAttachments, extract_contacts},
schema::SchemaTools,
account::{migration::AccountModel, stats::AccountStats},
common::{paginated::DataPage, signal::SIGNAL_MANAGER},
dashboard::{DashboardStats, Group, LargestEmail, TimeBucket},
error::{code::ErrorCode, BichonResult},
message::{
search::{EmailSearchFilter, SortBy},
tags::{TagAction, TagCount, TagsRequest},
},
raise_error,
settings::dir::DATA_DIR_MANAGER,
store::{
envelope::Envelope,
storage::BLOB_MANAGER,
tantivy::{
fatal_commit,
fields::{
F_ACCOUNT_ID, F_DATE, F_FROM, F_ID, F_REGULAR_ATTACHMENT_COUNT, F_SIZE, F_TAGS,
F_THREAD_ID, F_UID,
},
}, utc_now
model::{extract_contacts, EnvelopeWithAttachments},
schema::SchemaTools,
},
},
utc_now,
};
use chrono::Utc;
@@ -621,6 +629,51 @@ impl IndexManager {
Ok(Self::extract_max_uid(&agg_res))
}
pub async fn get_account_stats(&self, account_id: u64) -> BichonResult<AccountStats> {
let searcher = self.create_searcher()?;
let query = self.account_query(account_id);
let agg_req: Aggregations = serde_json::from_value(json!({
"total_count": {
"value_count": {
"field": F_ID
}
},
"total_size": {
"sum": {
"field": F_SIZE
}
}
}))
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
let collector = AggregationCollector::from_aggs(agg_req, Default::default());
let agg_res = searcher
.search(query.as_ref(), &collector)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
let mut stats = AccountStats::default();
stats.total_count = Self::extract_value_count(&agg_res, "total_count")?;
let result = agg_res.0.get("total_size").ok_or_else(|| {
raise_error!(
"missing 'total_size' aggregation result".into(),
ErrorCode::InternalError
)
})?;
if let AggregationResult::MetricResult(MetricResult::Sum(v)) = result {
stats.total_size = v.value.map(|v| v as u64).ok_or_else(|| {
raise_error!(
"'total_size' sum metric has no value".into(),
ErrorCode::InternalError
)
})?;
}
Ok(stats)
}
fn extract_max_uid(agg_res: &AggregationResults) -> Option<u64> {
agg_res.0.get("max_uid").and_then(|result| match result {
AggregationResult::MetricResult(MetricResult::Max(max)) => {
@@ -1110,13 +1163,13 @@ impl IndexManager {
let agg_res = searcher
.search(query.as_ref(), &collector)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
Self::extract_thread_count(&agg_res)
Self::extract_value_count(&agg_res, "thread_count")
}
fn extract_thread_count(agg_res: &AggregationResults) -> BichonResult<u64> {
let Some(result) = agg_res.0.get("thread_count") else {
fn extract_value_count(agg_res: &AggregationResults, name: &str) -> BichonResult<u64> {
let Some(result) = agg_res.0.get(name) else {
return Err(raise_error!(
"Missing aggregation result: thread_count".into(),
format!("Missing aggregation result: '{}'", name),
ErrorCode::InternalError
));
};