// // 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::modules::account::migration::AccountModel; use crate::modules::common::auth::ClientContext; use crate::modules::indexer::eml::EML_INDEX_MANAGER; use crate::modules::indexer::envelope::Envelope; 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::retrieve_attachment_content; use crate::modules::message::attachment::retrieve_nested_attachment_content; 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}; 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::rest::api::ApiTags; use crate::modules::rest::response::DataPage; use crate::modules::rest::ApiResult; use crate::modules::rest::ErrorCode; use crate::modules::users::permissions::Permission; use crate::modules::utils::validate_tag; use crate::raise_error; use poem::Body; use poem_openapi::param::{Path, Query}; use poem_openapi::payload::{Attachment, AttachmentType, Json}; use poem_openapi::OpenApi; use std::collections::HashMap; use std::collections::HashSet; pub struct MessageApi; #[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Message")] impl MessageApi { /// Deletes messages from a mailbox or moves them to the trash for the specified account. #[oai( path = "/delete-messages", method = "post", operation_id = "delete_messages" )] async fn delete_messages( &self, /// specifying the mailbox and messages to delete. payload: Json>>, context: ClientContext, ) -> ApiResult<()> { let request = payload.0; for account_id in request.keys() { context .require_permission(Some(*account_id), Permission::DATA_DELETE) .await?; } Ok(delete_messages_impl(request).await?) } /// Lists messages in a mailbox. Requires `mailbox_id`, `page`, and `page_size` query parameters. #[oai( path = "/list-messages/:account_id", method = "get", operation_id = "list_messages" )] async fn list_messages( &self, /// The ID of the account. account_id: Path, /// The ID of the mailbox to list messages from. mailbox_id: Query, page: Query, page_size: Query, context: ClientContext, ) -> ApiResult>> { let account_id = account_id.0; let mailbox_id = mailbox_id.0; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; Ok(Json( list_messages_impl(account_id, mailbox_id, page.0, page_size.0).await?, )) } /// Searches messages across all mailboxes using various filter criteria. /// The search filters are provided in the request body. #[oai( path = "/search-messages", method = "post", operation_id = "search_messages" )] async fn search_messages( &self, payload: Json, context: ClientContext, ) -> ApiResult>> { let authorized_ids: Option> = if context .has_permission(None, Permission::DATA_READ_ALL) .await { None } else { Some(context.user.account_access_map.keys().cloned().collect()) }; Ok(Json(search_messages_impl(authorized_ids, payload.0).await?)) } /// Retrieves all messages belonging to a specific thread. Requires `thread_id`, `page`, and `page_size` query parameters. #[oai( path = "/get-thread-messages/:account_id", method = "get", operation_id = "get_thread_messages" )] async fn get_thread_messages( &self, /// The ID of the account owning the mailbox. account_id: Path, // Thread ID thread_id: Query, /// The page number for pagination (1-based). page: Query, /// The number of messages per page. page_size: Query, context: ClientContext, ) -> ApiResult>> { let account_id = account_id.0; let thread_id = thread_id.0; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; Ok(Json( get_thread_messages(account_id, thread_id, page.0, page_size.0).await?, )) } /// Fetches the content of a specific email. #[oai( path = "/message-content/:account_id/:envelope_id", method = "get", operation_id = "fetch_message_content" )] async fn fetch_message_content( &self, /// The ID of the account. account_id: Path, /// The ID of the message to fetch. envelope_id: Path, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; Ok(Json( retrieve_email_content(account_id, envelope_id.0).await?, )) } /// Retrieves the content of an email embedded as an attachment. #[oai( path = "/nested-message-content/:account_id/:envelope_id", method = "get", operation_id = "fetch_nested_message_content" )] async fn fetch_nested_message_content( &self, /// The ID of the account. account_id: Path, /// The ID of the message to fetch. envelope_id: Path, content_hash: Query, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; let content_hash = content_hash.0.trim(); Ok(Json( retrieve_nested_eml_content(account_id, envelope_id.0, content_hash).await?, )) } /// Retrieves the envelope (metadata) of a specific message. #[oai( path = "/envelope/:account_id/:envelope_id", method = "get", operation_id = "get_envelope" )] async fn get_envelope( &self, /// The ID of the account. account_id: Path, /// The ID of the message. envelope_id: Path, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; let envelope_id = envelope_id.0; let envelope = ENVELOPE_INDEX_MANAGER .get_envelope_by_id(account_id, envelope_id.clone()) .await? .ok_or_else(|| { raise_error!( format!( "Envelope not found: account_id={} envelope_id={}", account_id, &envelope_id ), ErrorCode::ResourceNotFound ) })?; Ok(Json(envelope)) } /// Downloads the raw EML file of a specific email. #[oai( path = "/download-message/:account_id/:envelope_id", method = "get", operation_id = "download_message" )] async fn download_message( &self, /// The ID of the account. account_id: Path, /// The ID of the message to download. envelope_id: Path, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; AccountModel::check_account_exists(account_id).await?; context .require_permission(Some(account_id), Permission::DATA_RAW_DOWNLOAD) .await?; let envelope_id = envelope_id.0; let reader = EML_INDEX_MANAGER .get_reader(account_id, envelope_id.clone()) .await?; let body = Body::from_async_read(reader); let attachment = Attachment::new(body) .attachment_type(AttachmentType::Attachment) .filename(format!("{envelope_id}.eml")); Ok(attachment) } /// Restore an email to an account's IMAP server. #[oai( path = "/restore-messages/:account_id", method = "post", operation_id = "restore_messages" )] async fn restore_messages( &self, account_id: Path, /// Message IDs to restore. payload: Json, context: ClientContext, ) -> ApiResult<()> { let account_id = account_id.0; context .require_permission(Some(account_id), Permission::DATA_EXPORT_BATCH) .await?; Ok(restore_emails(account_id, payload.0.envelope_ids).await?) } /// Downloads a specific attachment from an email. Requires `name` query parameter. #[oai( path = "/download-attachment/:account_id/:envelope_id", method = "get", operation_id = "download_attachment" )] async fn download_attachment( &self, /// The ID of the account. account_id: Path, /// The ID of the message containing the attachment. envelope_id: Path, /// The content_hash of the attachment to download. content_hash: Query, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; let envelope_id = envelope_id.0.trim().to_string(); AccountModel::check_account_exists(account_id).await?; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; let content_hash = content_hash.0.trim(); let reader = retrieve_attachment_content(account_id, envelope_id, content_hash).await?; let body = Body::from_async_read(reader); let attachment = Attachment::new(body) .attachment_type(AttachmentType::Attachment) .filename(content_hash); Ok(attachment) } /// Downloads an attachment from within a nested email (EML file). #[oai( path = "/download-nested-attachment/:account_id/:envelope_id", method = "get", operation_id = "download_nested_attachment" )] async fn download_nested_attachment( &self, /// The ID of the account. account_id: Path, /// The ID of the message containing the attachment. envelope_id: Path, /// The filename of the attachment to download. content_hash: Query, nested_content_hash: Query, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; let envelope_id = envelope_id.0.trim().to_string(); AccountModel::check_account_exists(account_id).await?; context .require_permission(Some(account_id), Permission::DATA_READ) .await?; let content_hash = content_hash.0.trim(); let nested_content_hash = nested_content_hash.0.trim(); let reader = retrieve_nested_attachment_content( account_id, envelope_id, content_hash, nested_content_hash, ) .await?; let body = Body::from_async_read(reader); let attachment = Attachment::new(body) .attachment_type(AttachmentType::Attachment) .filename(nested_content_hash); Ok(attachment) } /// Returns all facets in the index along with their document counts. #[oai(path = "/all-tags", method = "get", operation_id = "get_all_tags")] async fn get_all_tags(&self, context: ClientContext) -> ApiResult>> { let authorized_ids: Option> = 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_all_tags(authorized_ids).await?, )) } /// Adds or removes facet tags for multiple emails across accounts. #[oai( path = "/update-tags", method = "post", operation_id = "update_envelope_tags" )] async fn update_envelope_tags( &self, req: Json, context: ClientContext, ) -> ApiResult<()> { let req = req.0; for tag in &req.tags { validate_tag(tag) .map_err(|e| raise_error!(format!("{}", e), ErrorCode::InvalidParameter))?; } for account_id in req.updates.keys() { context .require_permission(Some(*account_id), Permission::DATA_MANAGE) .await?; } ENVELOPE_INDEX_MANAGER .update_envelope_tags(req.updates, req.tags) .await?; Ok(()) } /// Retrieves a unique list of all contact email addresses across authorized accounts. #[oai( path = "/all-contacts", method = "get", operation_id = "get_all_contacts" )] async fn get_all_contacts(&self, context: ClientContext) -> ApiResult>> { let authorized_ids: Option> = 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_all_contacts(authorized_ids) .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> { let authorized_ids: Option> = 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?, )) } }