diff --git a/protos/rustmailer.proto b/protos/rustmailer.proto index e26cd41..4b85faf 100644 --- a/protos/rustmailer.proto +++ b/protos/rustmailer.proto @@ -1162,6 +1162,49 @@ message AppendReplyToDraftRequest { optional bool reply_all = 8; } +// Represents a tag (Label/Category/Flag) and its associated color +message TagAndColor { + // Name of the label, category, or IMAP custom flag + string name = 1; + + // Predefined Outlook category presets (e.g., "preset0", "preset1") + // Only used for Microsoft Graph API + optional string graph_color = 2; + + // Custom hex colors for Gmail API + optional LabelColor gmail_color = 3; +} + +// Defines the type of operation to be performed +enum TagAction { + // Adds tags to the specified messages + ADD = 0; + // Removes tags from the specified messages + REMOVE = 1; + // Overwrites the entire list of tags + SET = 2; +} + +// The main request for batch tagging operations +message BatchTagRequest { + // The ID of the email account + uint64 account_id = 1; + // A list of unique message identifiers + repeated string message_ids = 2; + + // The list of tags/categories with their optional color info + repeated TagAndColor tags = 3; + + // The action to perform (Add, Remove, or Set) + TagAction action = 4; + + // Required for IMAP context (e.g., "INBOX") + optional string mailbox_name = 5; + + // Whether to create tags if they don't exist (Gmail/Graph only) + optional bool auto_create_tags = 6; +} + // MessageService provides APIs for interacting with email messages. service MessageService { // Moves messages from one mailbox to another. @@ -1190,6 +1233,14 @@ service MessageService { rpc UnifiedSearch(UnifiedSearchRequest) returns (PagedMessages); // Creates a reply draft email linked to an existing message thread. rpc AppendReplyToDraft(AppendReplyToDraftRequest) returns (Empty); + // Batch modifies the custom tags, categories, or keywords on messages. + // This interface is dedicated to operating on **user-defined labels** and is separate + // from standard system status flags (like Read/Unread). + // It unifies tagging across different email services: + // - **Gmail/Graph API:** Operates on user-defined Label IDs or Category Names. + // - **IMAP/SMTP:** Operates on custom IMAP Keywords (Custom Flags). + // **Note:** This is a high-level operation designed for user tag management. + rpc TagMessages(BatchTagRequest) returns (Empty); } // Mta represents a Mail Transfer Agent configuration. diff --git a/src/modules/grpc/service/message/from.rs b/src/modules/grpc/service/message/from.rs index c5663d4..55b070c 100644 --- a/src/modules/grpc/service/message/from.rs +++ b/src/modules/grpc/service/message/from.rs @@ -23,6 +23,7 @@ use crate::modules::{ Condition, Conditions, Logic, MessageSearch, MessageSearchRequest, Operator, UnifiedSearchRequest, }, + tags::{BatchTagRequest, TagAction, TagAndColor}, transfer::MailboxTransferRequest, }, rest::response::{CursorDataPage, DataPage}, @@ -618,3 +619,39 @@ impl From for AppendReplyToDraftRequ } } } + +impl TryFrom for BatchTagRequest { + type Error = &'static str; + fn try_from(value: rustmailer_grpc::BatchTagRequest) -> Result { + Ok(Self { + message_ids: value.message_ids, + tags: value.tags.into_iter().map(|t| t.into()).collect(), + action: value.action.try_into()?, + mailbox_name: value.mailbox_name, + auto_create_tags: value.auto_create_tags, + }) + } +} + +impl From for TagAndColor { + fn from(value: rustmailer_grpc::TagAndColor) -> Self { + Self { + name: value.name, + graph_color: value.graph_color, + gmail_color: value.gmail_color.map(|c| c.into()), + } + } +} + +impl TryFrom for TagAction { + type Error = &'static str; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(TagAction::Add), + 1 => Ok(TagAction::Remove), + 2 => Ok(TagAction::Set), + _ => Err("Invalid value for TagAction"), + } + } +} diff --git a/src/modules/grpc/service/message/mod.rs b/src/modules/grpc/service/message/mod.rs index ca175d8..484a08d 100644 --- a/src/modules/grpc/service/message/mod.rs +++ b/src/modules/grpc/service/message/mod.rs @@ -8,7 +8,7 @@ use crate::modules::common::auth::ClientContext; use crate::modules::error::code::ErrorCode; use crate::modules::grpc::auth::require_account_access; use crate::modules::grpc::service::rustmailer_grpc::{ - AppendReplyToDraftRequest, ByteResponse, CursorDataPage, EmailEnvelopeList, + AppendReplyToDraftRequest, BatchTagRequest, ByteResponse, CursorDataPage, EmailEnvelopeList, GetThreadMessagesRequest, ListThreadsRequest, MessageContentResponse, PagedMessages, UnifiedSearchRequest, }; @@ -29,6 +29,9 @@ use crate::modules::message::list::{ }; use crate::modules::message::search::payload::MessageSearchRequest as RustMailerMessageSearchRequest; use crate::modules::message::search::payload::UnifiedSearchRequest as RustMailerUnifiedSearchRequest; +use crate::modules::message::tags::{ + tag_messages_impl, BatchTagRequest as RustMailerBatchTagRequest, +}; use crate::modules::message::transfer::{transfer_messages, MessageTransfer}; use crate::raise_error; use poem_grpc::{Request, Response, Status}; @@ -257,4 +260,17 @@ impl MessageService for RustMailerMessageService { request.append_reply_to_draft(account_id).await?; Ok(Response::new(Empty::default())) } + + async fn tag_messages( + &self, + request: Request, + ) -> Result, Status> { + let req = require_account_access(request, |r| r.account_id)?; + let account_id = req.account_id; + let request: RustMailerBatchTagRequest = req + .try_into() + .map_err(|e: &'static str| raise_error!(e.to_string(), ErrorCode::InvalidParameter))?; + tag_messages_impl(account_id, request).await?; + Ok(Response::new(Empty::default())) + } } diff --git a/src/modules/grpc/tests.rs b/src/modules/grpc/tests.rs index 2c16b19..81f6dfc 100644 --- a/src/modules/grpc/tests.rs +++ b/src/modules/grpc/tests.rs @@ -11,9 +11,10 @@ use crate::{ common::rustls::RustMailerTls, context::Initialize, grpc::service::rustmailer_grpc::{ - AppendReplyToDraftRequest, ExternalOAuth2Request, GetThreadMessagesRequest, - ListMessagesRequest, ListThreadsRequest, MessageServiceClient, OAuth2ServiceClient, - TemplateSentTestRequest, TemplatesServiceClient, UnifiedSearchRequest, + AppendReplyToDraftRequest, BatchTagRequest, ExternalOAuth2Request, + GetThreadMessagesRequest, ListMessagesRequest, ListThreadsRequest, + MessageServiceClient, OAuth2ServiceClient, TagAndColor, TemplateSentTestRequest, + TemplatesServiceClient, UnifiedSearchRequest, }, }, }; @@ -84,7 +85,6 @@ async fn test2() { } #[tokio::test] - async fn test3() { RustMailerTls::initialize().await.unwrap(); @@ -264,3 +264,35 @@ async fn test8() { ); grpc_client.append_reply_to_draft(request).await.unwrap(); } + +#[tokio::test] +async fn test9() { + RustMailerTls::initialize().await.unwrap(); + let cfg = ClientConfig::builder() + .uri("http://localhost:16630") + .build() + .unwrap(); + let mut grpc_client = MessageServiceClient::new(cfg); + grpc_client.set_accept_compressed([CompressionEncoding::GZIP]); + grpc_client.set_send_compressed(CompressionEncoding::GZIP); + + let request = BatchTagRequest { + account_id: 3815676991897752, + message_ids: vec!["AQMkADAwATMwMAItNzE0OC1jZTEzLTAwAi0wMAoARgAAA_KUk7xWPSBEntPHShr61lgHAOo9V4GwHndCjf0x1uoIcwUAAAIBDAAAAOo9V4GwHndCjf0x1uoIcwUAAckOKwUAAAA=".into()], + tags: vec![TagAndColor { + name: "test_name2".into(), + graph_color: Some("preset1".into()), + gmail_color: None + }], + action: 2, + mailbox_name: Some("INBOX".into()), + auto_create_tags: Some(true), + }; + + let mut request = poem_grpc::Request::new(request); + request.metadata_mut().insert( + AUTHORIZATION, + format!("Bearer {}", "0ZRTSl2WhTOUQYMCgSm45i1o"), + ); + grpc_client.tag_messages(request).await.unwrap(); +}