mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat(search): add advanced attachment filters for extension, category and mime type
This commit is contained in:
@@ -37,6 +37,7 @@ use crate::{
|
||||
manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
|
||||
},
|
||||
message::{
|
||||
attachment::AttachmentMetadata,
|
||||
content::AttachmentInfo,
|
||||
search::{SearchFilter, SortBy},
|
||||
tags::TagCount,
|
||||
@@ -246,9 +247,9 @@ impl DuckDBManager {
|
||||
att.filename,
|
||||
att.get_extension(),
|
||||
att.get_category(),
|
||||
att.file_type,
|
||||
att.file_type.to_ascii_lowercase(),
|
||||
att.size as u64,
|
||||
env.content_hash.clone(), //这里是错误的,应该保存附件的content_hash
|
||||
env.content_hash.clone(), // It's the hash of the attachment content itself, not the hash of the full email.
|
||||
0,
|
||||
])
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
@@ -550,6 +551,55 @@ impl DuckDBManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_attachment_metadata(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<AttachmentMetadata> {
|
||||
let conn = self.conn()?;
|
||||
let mut sql = r#"
|
||||
SELECT
|
||||
CAST(array_agg(DISTINCT extension) AS JSON) AS extensions,
|
||||
CAST(array_agg(DISTINCT ext_category) AS JSON) AS categories,
|
||||
CAST(array_agg(DISTINCT content_type) AS JSON) AS content_types
|
||||
FROM envelope_attachments
|
||||
"#
|
||||
.to_string();
|
||||
|
||||
let mut params_vec: Vec<duckdb::types::Value> = Vec::new();
|
||||
if let Some(ref acc_set) = accounts {
|
||||
if !acc_set.is_empty() {
|
||||
let placeholders = vec!["?"; acc_set.len()].join(", ");
|
||||
sql.push_str(&format!(" WHERE account_id IN ({})", placeholders));
|
||||
|
||||
for &id in acc_set {
|
||||
params_vec.push(id.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
|
||||
let result = stmt
|
||||
.query_row(duckdb::params_from_iter(params_vec), |row| {
|
||||
let exts_raw: String = row.get(0)?;
|
||||
let cats_raw: String = row.get(1)?;
|
||||
let ctypes_raw: String = row.get(2)?;
|
||||
let exts: Vec<String> = serde_json::from_str(&exts_raw).unwrap_or_default();
|
||||
let cats: Vec<String> = serde_json::from_str(&cats_raw).unwrap_or_default();
|
||||
let ctypes: Vec<String> = serde_json::from_str(&ctypes_raw).unwrap_or_default();
|
||||
|
||||
Ok(AttachmentMetadata {
|
||||
extensions: exts.into_iter().collect(),
|
||||
categories: cats.into_iter().collect(),
|
||||
content_types: ctypes.into_iter().collect(),
|
||||
})
|
||||
})
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn delete_envelopes_multi_account(
|
||||
&self,
|
||||
deletes: HashMap<u64, Vec<String>>,
|
||||
@@ -1122,7 +1172,10 @@ impl DuckDBManager {
|
||||
|
||||
let mut base_sql = String::from(" FROM envelopes e ");
|
||||
|
||||
let need_join_attachment = filter.attachment_name.is_some();
|
||||
let need_join_attachment = filter.attachment_name.is_some()
|
||||
|| filter.attachment_extension.is_some()
|
||||
|| filter.attachment_category.is_some()
|
||||
|| filter.attachment_content_type.is_some();
|
||||
|
||||
if need_join_attachment {
|
||||
base_sql.push_str(
|
||||
@@ -1290,6 +1343,21 @@ impl DuckDBManager {
|
||||
args.push(format!("%{}%", name).into());
|
||||
}
|
||||
|
||||
if let Some(ext) = filter.attachment_extension {
|
||||
base_sql.push_str(" AND a.extension ILIKE ? ");
|
||||
args.push(format!("%{}%", ext).into());
|
||||
}
|
||||
|
||||
if let Some(cat) = filter.attachment_category {
|
||||
base_sql.push_str(" AND a.ext_category ILIKE ? ");
|
||||
args.push(format!("%{}%", cat).into());
|
||||
}
|
||||
|
||||
if let Some(ctype) = filter.attachment_content_type {
|
||||
base_sql.push_str(" AND a.content_type ILIKE ? ");
|
||||
args.push(format!("%{}%", ctype).into());
|
||||
}
|
||||
|
||||
let count_sql = if need_join_attachment {
|
||||
format!("SELECT COUNT(DISTINCT e.id) {}", base_sql)
|
||||
} else {
|
||||
|
||||
@@ -160,7 +160,7 @@ fn extract_envelope_core(
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "application/octet-stream".to_string());
|
||||
|
||||
//注意:有些附件是没有名字的,这样extension也就不存在,那么在获取附件的时候,就不能通过name定位
|
||||
Some(AttachmentInfo {
|
||||
filename: attachment
|
||||
.attachment_name()
|
||||
|
||||
@@ -25,7 +25,9 @@ use std::{
|
||||
|
||||
use crate::modules::{
|
||||
duckdb::init::duckdb,
|
||||
message::{content::AttachmentInfo, search::SortBy, tags::TagCount},
|
||||
message::{
|
||||
attachment::AttachmentMetadata, content::AttachmentInfo, search::SortBy, tags::TagCount,
|
||||
},
|
||||
settings::cli::SETTINGS,
|
||||
};
|
||||
use crate::{
|
||||
@@ -192,6 +194,15 @@ impl EnvelopeIndexManager {
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn get_attachment_metadata(
|
||||
&self,
|
||||
accounts: Option<HashSet<u64>>,
|
||||
) -> BichonResult<AttachmentMetadata> {
|
||||
tokio::task::spawn_blocking(move || duckdb()?.get_attachment_metadata(accounts))
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
|
||||
}
|
||||
|
||||
pub async fn delete_envelopes_multi_account(
|
||||
&self,
|
||||
deletes: HashMap<u64, Vec<String>>, // HashMap<account_id, envelope_ids>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
pub struct AttachmentMetadata {
|
||||
/// A collection of unique file extensions found in attachments.
|
||||
/// Example: ["pdf", "docx", "png"]
|
||||
pub extensions: HashSet<String>,
|
||||
|
||||
/// A collection of high-level attachment categories.
|
||||
/// Example: ["document", "image", "archive"]
|
||||
pub categories: HashSet<String>,
|
||||
|
||||
/// A collection of unique MIME types (Content-Type) for the attachments.
|
||||
/// Example: ["application/pdf", "image/jpeg"]
|
||||
pub content_types: HashSet<String>,
|
||||
}
|
||||
@@ -51,7 +51,7 @@ impl AttachmentInfo {
|
||||
std::path::Path::new(&self.filename)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(|ext| ext.to_lowercase())
|
||||
.map(|ext| ext.to_ascii_lowercase())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod append;
|
||||
pub mod attachment;
|
||||
pub mod contacts;
|
||||
pub mod content;
|
||||
pub mod delete;
|
||||
|
||||
@@ -50,6 +50,9 @@ pub struct SearchFilter {
|
||||
pub has_attachment: Option<bool>,
|
||||
pub attachment_name: Option<String>,
|
||||
pub tags: Option<HashSet<String>>,
|
||||
pub attachment_extension: Option<String>,
|
||||
pub attachment_category: Option<String>,
|
||||
pub attachment_content_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Enum)]
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::modules::indexer::manager::EML_INDEX_MANAGER;
|
||||
use crate::modules::indexer::manager::ENVELOPE_INDEX_MANAGER;
|
||||
use crate::modules::message::append::restore_emails;
|
||||
use crate::modules::message::append::RestoreMessagesRequest;
|
||||
use crate::modules::message::attachment::AttachmentMetadata;
|
||||
use crate::modules::message::content::retrieve_nested_eml_content;
|
||||
use crate::modules::message::content::FullNestedMessageContent;
|
||||
use crate::modules::message::content::{retrieve_email_content, FullMessageContent};
|
||||
@@ -411,4 +412,29 @@ impl MessageApi {
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Retrieves unique metadata for all attachments across authorized accounts.
|
||||
#[oai(
|
||||
path = "/attachment_metadata",
|
||||
method = "get",
|
||||
operation_id = "get_attachment_metadata"
|
||||
)]
|
||||
async fn get_attachment_metadata(
|
||||
&self,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<AttachmentMetadata>> {
|
||||
let authorized_ids: Option<HashSet<u64>> = if context
|
||||
.has_permission(None, Permission::DATA_READ_ALL)
|
||||
.await
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(context.user.account_access_map.keys().cloned().collect())
|
||||
};
|
||||
Ok(Json(
|
||||
ENVELOPE_INDEX_MANAGER
|
||||
.get_attachment_metadata(authorized_ids)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user