// 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::model::Envelope; 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, FullMessageContent, MessageContentRequest, }; use crate::modules::message::delete::{move_to_trash, MessageDeleteRequest}; use crate::modules::message::flag::{modify_flags, FlagMessageRequest}; use crate::modules::message::full::retrieve_raw_email; use crate::modules::message::list::{ get_thread_messages, list_messages_in_mailbox, list_threads_in_mailbox, }; use crate::modules::message::search::payload::{MessageSearchRequest, UnifiedSearchRequest}; use crate::modules::message::transfer::{ transfer_messages, MailboxTransferRequest, MessageTransfer, }; use crate::modules::rest::api::ApiTags; use crate::modules::rest::response::{CursorDataPage, 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(transfer_messages(account_id, &payload.0, MessageTransfer::Move).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(transfer_messages(account_id, &payload.0, MessageTransfer::Copy).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(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 token for fetching the next page of results in pagination. /// /// - If `None`, this indicates that the first page should be returned. /// - If `Some(token)`, the page corresponding to this token will be fetched. next_page_token: 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(), next_page_token.0.as_deref(), 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, // 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, 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 account_id = account_id.0; context.require_account_access(account_id)?; let (reader, filename) = 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 = "/raw-message/:account_id", method = "get", operation_id = "fetch_raw_message" )] async fn fetch_raw_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 unique ID of the message, either IMAP UID or Gmail API MID. /// - For IMAP accounts, this is the UID converted to a string. It must be a valid numeric string /// that can be parsed back to a `u32`. /// - For Gmail API accounts, this is the message ID (`mid`) returned by the API. id: 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 mailbox_opt = mailbox.0.as_ref().map(|m| m.trim().to_owned()); let id = id.0.trim(); let reader = retrieve_raw_email( account_id, mailbox_opt.as_deref(), id ) .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 token for fetching the next page of results in pagination. /// /// - If `None`, this indicates that the first page should be returned. /// - If `Some(token)`, the page corresponding to this token will be fetched. next_page_token: Query>, /// The number of messages per page. page_size: Query, /// If `true`, lists results in descending order; otherwise, ascending. imap account only 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_impl(account_id, next_page_token.0.as_deref(), 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(()) } }