feat: Ability to add/remove tags from any list of messages #189

This commit is contained in:
rustmailer
2026-03-26 21:22:21 +08:00
parent 5d0039cb74
commit 3f11c5dbbf
30 changed files with 805 additions and 66 deletions
+24 -12
View File
@@ -40,7 +40,7 @@ use crate::{
attachment::AttachmentMetadata,
content::{AttachmentDetail, AttachmentInfo},
search::{SearchFilter, SortBy},
tags::TagCount,
tags::{TagAction, TagCount, TagsRequest},
},
rest::response::DataPage,
settings::{cli::SETTINGS, dir::DATA_DIR_MANAGER},
@@ -611,20 +611,24 @@ impl DuckDBManager {
Ok(contacts)
}
pub fn update_envelope_tags(
&self,
updates: HashMap<u64, Vec<String>>,
tags: Vec<String>,
) -> BichonResult<()> {
pub fn update_envelope_tags(&self, request: TagsRequest) -> BichonResult<()> {
let mut conn = self.conn()?;
let tags_json = serde_json::to_string(&tags)
let tags_json = serde_json::to_string(&request.tags)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
let set_clause = match &request.action {
TagAction::Overwrite => "SET tags = ?::JSON::VARCHAR[]",
TagAction::Add => "SET tags = list_distinct(list_concat(tags, ?::JSON::VARCHAR[]))",
TagAction::Remove => {
"SET tags = list_filter(tags, x -> NOT list_contains(?::JSON::VARCHAR[], x))"
}
};
let tx = conn
.transaction()
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
for (account_id, ids) in updates {
for (account_id, ids) in request.updates {
if ids.is_empty() {
continue;
}
@@ -633,23 +637,31 @@ impl DuckDBManager {
let placeholders = vec!["?"; chunk.len()].join(", ");
let query = format!(
"UPDATE envelopes
SET tags = CAST(json(?) AS VARCHAR[])
{}
WHERE account_id = ?
AND id IN ({})",
placeholders
set_clause, placeholders
);
let mut params: Vec<Box<dyn duckdb::ToSql>> = Vec::new();
let mut params: Vec<Box<dyn duckdb::ToSql>> = Vec::with_capacity(chunk.len() + 2);
params.push(Box::new(tags_json.clone()));
params.push(Box::new(account_id));
for id in chunk {
params.push(Box::new(id.clone()));
}
let param_refs: Vec<&dyn duckdb::ToSql> =
params.iter().map(|p| p.as_ref()).collect();
tx.execute(&query, duckdb::params_from_iter(param_refs))
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
.map_err(|e| {
raise_error!(
format!("Update failed. Account: {}, Error: {}", account_id, e),
ErrorCode::InternalError
)
})?;
}
}
+5 -9
View File
@@ -28,7 +28,7 @@ use crate::modules::{
attachment::AttachmentMetadata,
content::{AttachmentDetail, AttachmentInfo},
search::SortBy,
tags::TagCount,
tags::{TagCount, TagsRequest},
},
};
use crate::{
@@ -204,16 +204,12 @@ impl EnvelopeIndexManager {
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
}
pub async fn update_envelope_tags(
&self,
updates: HashMap<u64, Vec<String>>, // HashMap<account_id, envelope_ids>
tags: Vec<String>,
) -> BichonResult<()> {
if updates.is_empty() {
tracing::warn!("update_envelope_tags: updates is empty, nothing to update");
pub async fn update_envelope_tags(&self, request: TagsRequest) -> BichonResult<()> {
if request.updates.is_empty() {
tracing::warn!("update_envelope_tags: request is empty, nothing to update");
return Ok(());
}
tokio::task::spawn_blocking(move || duckdb()?.update_envelope_tags(updates, tags))
tokio::task::spawn_blocking(move || duckdb()?.update_envelope_tags(request))
.await
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
}
+11 -3
View File
@@ -16,16 +16,16 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashMap;
use poem_openapi::Object;
use poem_openapi::{Enum, Object};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct UpdateTagsRequest {
pub struct TagsRequest {
pub updates: HashMap<u64, Vec<String>>, // account_id -> envelope_ids
pub tags: Vec<String>,
pub action: TagAction,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
@@ -33,3 +33,11 @@ pub struct TagCount {
pub tag: String,
pub count: u64,
}
#[derive(Enum, Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum TagAction {
Add,
Remove,
#[default]
Overwrite,
}
+3 -3
View File
@@ -33,7 +33,7 @@ use crate::modules::message::delete::delete_messages_impl;
use crate::modules::message::list::{get_thread_messages, list_messages_impl};
use crate::modules::message::search::{search_messages_impl, SearchRequest};
use crate::modules::message::tags::TagCount;
use crate::modules::message::tags::UpdateTagsRequest;
use crate::modules::message::tags::TagsRequest;
use crate::modules::rest::api::ApiTags;
use crate::modules::rest::response::DataPage;
use crate::modules::rest::ApiResult;
@@ -376,7 +376,7 @@ impl MessageApi {
)]
async fn update_envelope_tags(
&self,
req: Json<UpdateTagsRequest>,
req: Json<TagsRequest>,
context: ClientContext,
) -> ApiResult<()> {
let req = req.0;
@@ -392,7 +392,7 @@ impl MessageApi {
}
ENVELOPE_INDEX_MANAGER
.update_envelope_tags(req.updates, req.tags)
.update_envelope_tags(req)
.await?;
Ok(())
}