feat(grpc): add tag_messages support for message tagging via gRPC

This commit is contained in:
rustmailer
2026-03-05 11:42:29 +08:00
parent d0a6fdd347
commit f379beb182
4 changed files with 141 additions and 5 deletions
+51
View File
@@ -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.
+37
View File
@@ -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<rustmailer_grpc::AppendReplyToDraftRequest> for AppendReplyToDraftRequ
}
}
}
impl TryFrom<rustmailer_grpc::BatchTagRequest> for BatchTagRequest {
type Error = &'static str;
fn try_from(value: rustmailer_grpc::BatchTagRequest) -> Result<Self, Self::Error> {
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<rustmailer_grpc::TagAndColor> 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<i32> for TagAction {
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(TagAction::Add),
1 => Ok(TagAction::Remove),
2 => Ok(TagAction::Set),
_ => Err("Invalid value for TagAction"),
}
}
}
+17 -1
View File
@@ -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<BatchTagRequest>,
) -> Result<Response<Empty>, 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()))
}
}
+36 -4
View File
@@ -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();
}