// // Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use crate::common::auth::WrappedContext; use crate::rest::api::ApiTags; use crate::rest::ApiResult; use bichon_core::common::paginated::DataPage; use bichon_core::error::code::ErrorCode; use bichon_core::message::attachment::AttachmentMetadata; use bichon_core::message::search::search_attachment_impl; use bichon_core::message::search::AttachmentSearchRequest; use bichon_core::message::tags::TagCount; use bichon_core::message::tags::TagsRequest; use bichon_core::raise_error; use bichon_core::store::tantivy::attachment::ATTACHMENT_MANAGER; use bichon_core::store::tantivy::model::AttachmentModel; use bichon_core::store::tantivy::validate_facet; use bichon_core::users::permissions::Permission; use poem_openapi::param::Path; use poem_openapi::payload::Json; use poem_openapi::OpenApi; use std::collections::HashSet; pub struct AttachmentApi; #[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Attachment")] impl AttachmentApi { /// Searches messages across all mailboxes using various filter criteria. /// The search filters are provided in the request body. #[oai( path = "/search-attachment", method = "post", operation_id = "search_attachment" )] async fn search_attachment( &self, payload: Json, context: WrappedContext, ) -> ApiResult>> { let authorized_ids: Option> = if context.has_permission(None, Permission::DATA_READ_ALL) { None } else { Some(context.user.account_access_map.keys().cloned().collect()) }; Ok(Json(search_attachment_impl(authorized_ids, payload.0)?)) } /// Retrieves the attachment (metadata) of a specific message. #[oai( path = "/attachment/:account_id/:attachment_id", method = "get", operation_id = "get_attachment" )] async fn get_attachment( &self, /// The ID of the account. account_id: Path, /// The ID of the attachment. attachment_id: Path, context: WrappedContext, ) -> ApiResult> { let account_id = account_id.0; context.require_permission(Some(account_id), Permission::DATA_READ)?; let attachment_id = attachment_id.0; let a = ATTACHMENT_MANAGER .get_attachment_by_id(account_id, &attachment_id)? .ok_or_else(|| { raise_error!( format!( "Attachment not found: account_id={} envelope_id={}", account_id, &attachment_id ), ErrorCode::ResourceNotFound ) })?; Ok(Json(a)) } /// Returns all facets in the index along with their document counts. #[oai( path = "/all-attachment-tags", method = "get", operation_id = "get_all_attachment_tags" )] async fn get_all_attachment_tags( &self, context: WrappedContext, ) -> ApiResult>> { let authorized_ids: Option> = if context.has_permission(None, Permission::DATA_READ_ALL) { None } else { Some(context.user.account_access_map.keys().cloned().collect()) }; Ok(Json(ATTACHMENT_MANAGER.get_all_tags(authorized_ids)?)) } /// Adds or removes facet tags for multiple emails across accounts. #[oai( path = "/update-attachment-tags", method = "post", operation_id = "update_attachment_tags" )] async fn update_attachment_tags( &self, req: Json, context: WrappedContext, ) -> ApiResult<()> { let req = req.0; for tag in &req.tags { validate_facet(tag)?; } for account_id in req.updates.keys() { context.require_permission(Some(*account_id), Permission::DATA_MANAGE)?; } ATTACHMENT_MANAGER.update_attachment_tags(req).await?; Ok(()) } /// Retrieves a unique list of all contact email addresses across authorized accounts. #[oai( path = "/attachment-senders", method = "get", operation_id = "get_attachment_senders" )] async fn get_attachment_senders( &self, context: WrappedContext, ) -> ApiResult>> { let authorized_ids: Option> = if context.has_permission(None, Permission::DATA_READ_ALL) { None } else { Some(context.user.account_access_map.keys().cloned().collect()) }; Ok(Json(ATTACHMENT_MANAGER.get_all_senders(authorized_ids)?)) } /// 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: WrappedContext, ) -> ApiResult> { let authorized_ids: Option> = if context.has_permission(None, Permission::DATA_READ_ALL) { None } else { Some(context.user.account_access_map.keys().cloned().collect()) }; Ok(Json( ATTACHMENT_MANAGER.collect_attachment_metadata(authorized_ids)?, )) } }