// Copyright © 2025 rustmailer.com // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. use crate::current_datetime; use crate::modules::cache::imap::v2::EmailEnvelopeV2; use crate::modules::common::auth::ClientContext; use crate::modules::message::append::AppendReplyToDraftRequest; use crate::modules::message::attachment::{retrieve_email_attachment, AttachmentRequest}; use crate::modules::message::content::{ retrieve_email_content, MessageContent, MessageContentRequest, }; use crate::modules::message::copy::{copy_mailbox_messages, MailboxTransferRequest}; use crate::modules::message::delete::{ move_to_trash_or_delete_messages_directly, MessageDeleteRequest, }; use crate::modules::message::flag::{modify_flags, FlagMessageRequest}; use crate::modules::message::full::retrieve_full_email; use crate::modules::message::list::{ get_thread_messages, list_messages_in_mailbox, list_threads_in_mailbox, }; use crate::modules::message::mv::move_mailbox_messages; use crate::modules::message::search::payload::{MessageSearchRequest, UnifiedSearchRequest}; use crate::modules::rest::api::ApiTags; use crate::modules::rest::response::DataPage; use crate::modules::rest::ApiResult; use poem::web::Path; use poem::Body; use poem_openapi::param::Query; use poem_openapi::payload::{Attachment, AttachmentType, Json}; use poem_openapi::OpenApi; pub struct MessageApi; #[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Message")] impl MessageApi { /// Moves messages from one mailbox to another for the specified account. #[oai( path = "/move-messages/:account_id", method = "post", operation_id = "move_messages" )] async fn move_messages( &self, /// The ID of the account owning the mailboxes. account_id: Path, /// specifying the source and destination mailboxes and messages payload: Json, context: ClientContext, ) -> ApiResult<()> { let account_id = account_id.0; context.require_account_access(account_id)?; Ok(move_mailbox_messages(account_id, &payload.0).await?) } /// Copies messages from one mailbox to another for the specified account. #[oai( path = "/copy-messages/:account_id", method = "post", operation_id = "copy_messages" )] async fn copy_messages( &self, /// The ID of the account owning the mailboxes. account_id: Path, /// specifying the source and destination mailboxes and messages. payload: Json, context: ClientContext, ) -> ApiResult<()> { let account_id = account_id.0; context.require_account_access(account_id)?; Ok(copy_mailbox_messages(account_id, &payload.0).await?) } /// Deletes messages from a mailbox or moves them to the trash for the specified account. #[oai( path = "/delete-messages/:account_id", method = "post", operation_id = "delete_messages" )] async fn delete_messages( &self, /// The ID of the account owning the mailboxes. account_id: Path, /// specifying the mailbox and messages to delete. payload: Json, context: ClientContext, ) -> ApiResult<()> { let account_id = account_id.0; context.require_account_access(account_id)?; Ok(move_to_trash_or_delete_messages_directly(account_id, &payload.0).await?) } /// Updates flags on messages in a mailbox for the specified account. #[oai( path = "/flag-messages/:account_id", method = "post", operation_id = "update_message_flags" )] async fn update_message_flags( &self, /// The ID of the account owning the mailbox. account_id: Path, /// specifying the mailbox, messages, and flags to modify. payload: Json, context: ClientContext, ) -> ApiResult<()> { let account_id = account_id.0; context.require_account_access(account_id)?; Ok(modify_flags(account_id, payload.0).await?) } /// Lists messages in a specified mailbox for the given account. #[oai( path = "/list-messages/:account_id", method = "get", operation_id = "list_messages" )] async fn list_messages( &self, /// The ID of the account owning the mailbox. account_id: Path, /// The decoded, human-readable name of the mailbox containing the email (e.g., "INBOX"). /// This name is presented as it appears to users, with any encoding (e.g., UTF-7) automatically handled by the system, /// so no manual decoding is required. mailbox: Query, /// fetches messages from the IMAP server; otherwise, uses local data. remote: Query>, /// The page number for pagination (1-based). page: Query, /// The number of messages per page. page_size: Query, /// lists messages in descending order; otherwise, ascending. internal date desc: Query>, context: ClientContext, ) -> ApiResult>> { let remote = remote.0.unwrap_or(false); let desc = desc.0.unwrap_or(false); let account_id = account_id.0; context.require_account_access(account_id)?; Ok(Json( list_messages_in_mailbox( account_id, mailbox.0.trim(), page.0, page_size.0, remote, desc, ) .await?, )) } /// Lists threads in a specified mailbox for the given account. #[oai( path = "/list-threads/:account_id", method = "get", operation_id = "list_threads" )] async fn list_threads( &self, /// The ID of the account owning the mailbox. account_id: Path, /// The decoded, human-readable name of the mailbox containing the email (e.g., "INBOX"). /// This name is presented as it appears to users, with any encoding (e.g., UTF-7) automatically handled by the system, /// so no manual decoding is required. mailbox: Query, /// The page number for pagination (1-based). page: Query, /// The number of messages per page. page_size: Query, /// lists messages in descending order; otherwise, ascending. internal date desc: Query>, context: ClientContext, ) -> ApiResult>> { let desc = desc.0.unwrap_or(false); let account_id = account_id.0; context.require_account_access(account_id)?; Ok(Json( list_threads_in_mailbox(account_id, mailbox.0.trim(), page.0, page_size.0, desc) .await?, )) } /// Get thread's envelopes in a specified mailbox for the given account. #[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, /// The decoded, human-readable name of the mailbox containing the email (e.g., "INBOX"). /// This name is presented as it appears to users, with any encoding (e.g., UTF-7) automatically handled by the system, /// so no manual decoding is required. mailbox: Query, // Thread ID thread_id: Query, context: ClientContext, ) -> ApiResult>> { let account_id = account_id.0; context.require_account_access(account_id)?; Ok(Json( get_thread_messages(account_id, mailbox.0.trim(), thread_id.0).await?, )) } /// Fetches the content of a specific email for the given account. #[oai( path = "/message-content/:account_id", method = "post", operation_id = "fetch_message_content" )] async fn fetch_message_content( &self, /// The ID of the account owning the mailbox. account_id: Path, /// specifying the mailbox and message to fetch. payload: Json, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; context.require_account_access(account_id)?; Ok(Json( retrieve_email_content(account_id, payload.0, false).await?, )) } /// Fetches an attachment from a specific email for the given account. #[oai( path = "/message-attachment/:account_id", method = "post", operation_id = "fetch_message_attachment" )] async fn fetch_message_attachment( &self, /// The ID of the account owning the mailbox. account_id: Path, /// specifying the mailbox, message, and attachment to fetch. payload: Json, context: ClientContext, ) -> ApiResult> { let request = payload.0; let filename = request.attachment.filename.clone(); let account_id = account_id.0; context.require_account_access(account_id)?; let reader = retrieve_email_attachment(account_id, request).await?; let body = Body::from_async_read(reader); let mut attachment = Attachment::new(body).attachment_type(AttachmentType::Attachment); if let Some(filename) = filename { attachment = attachment.filename(filename); } Ok(attachment) } /// Fetches the full content of a specific email for the given account. #[oai( path = "/full-message/:account_id", method = "get", operation_id = "fetch_full_message" )] async fn fetch_full_message( &self, /// The ID of the account owning the mailbox. account_id: Path, /// The decoded, human-readable name of the mailbox containing the email (e.g., "INBOX"). /// This name is presented as it appears to users, with any encoding (e.g., UTF-7) automatically handled by the system, /// so no manual decoding is required. mailbox: Query, /// The IMAP UID of the email to fetch. uid: Query, /// An optional filename for the attachment (defaults to a timestamped `.elm` file). filename: Query>, context: ClientContext, ) -> ApiResult> { let account_id = account_id.0; context.require_account_access(account_id)?; let filename = filename.0.unwrap_or(format!("{}.elm", current_datetime!())); let reader = retrieve_full_email(account_id, mailbox.0, uid.0).await?; let body = Body::from_async_read(reader); let attachment = Attachment::new(body) .attachment_type(AttachmentType::Attachment) .filename(filename); Ok(attachment) } /// Searches for messages in mailboxes for the specified account. performs the search on the IMAP server; #[oai( path = "/search-message/:account_id", method = "post", operation_id = "search_messages" )] async fn search_messages( &self, /// The ID of the account owning the mailboxes. account_id: Path, /// The page number for pagination (1-based). page: Query, /// The number of messages per page. page_size: Query, /// If `true`, lists results in descending order; otherwise, ascending. desc: Query>, /// specifying the search criteria (e.g., keywords, flags). payload: Json, context: ClientContext, ) -> ApiResult>> { let request = payload.0; let desc = desc.0.unwrap_or(false); let account_id = account_id.0; context.require_account_access(account_id)?; Ok(Json( request .search(account_id, page.0, page_size.0, desc) .await?, )) } /// Searches for messages from local cache for the specified account(s). /// This performs a unified search across mailboxes based on indexed data, /// without querying the remote IMAP server. #[oai( path = "/unified-search", method = "post", operation_id = "unified_search" )] async fn unified_search( &self, /// The page number for pagination (1-based). page: Query, /// The number of messages per page. page_size: Query, /// If `true`, lists results in descending order; otherwise, ascending. desc: Query>, /// The unified search criteria (email, time range, accounts, etc.). payload: Json, /// Request context (includes authentication and permissions). context: ClientContext, ) -> ApiResult>> { let mut request = payload.0; let desc = desc.0.unwrap_or(false); // Ensure account access control match &mut request.accounts { Some(accounts) => { for &account_id in accounts.iter() { context.require_account_access(account_id)?; } } None => { if !context.is_root { // Inject accessible accounts if not specified if let Some(accessible) = context.accessible_accounts()? { let account_ids = accessible.iter().map(|a| a.id).collect::>(); request.accounts = Some(account_ids); } } } } Ok(Json(request.search(page.0, page_size.0, desc).await?)) } /// Creates a reply draft email for the specified account. /// The server internally constructs the reply email, automatically linking it to the /// original email thread by applying appropriate headers such as `References` and `In-Reply-To`. /// /// The newly created draft is appended into the specified draft mailbox. #[oai( path = "/append-reply-to-draft/:account_id", method = "post", operation_id = "append_reply_to_draft" )] async fn append_reply_to_draft( &self, /// The ID of the email account for which the draft is created. account_id: Path, /// Request body containing original message location and reply content. payload: Json, /// Request context (authentication, authorization). context: ClientContext, ) -> ApiResult<()> { let account_id = account_id.0; // Verify that the client has permission to access the account. context.require_account_access(account_id)?; // Perform the draft creation and append operation. payload.0.append_reply_to_draft(account_id).await?; Ok(()) } }