From 60041ee59fe4769ccef15f03e79cfb139edb3a6b Mon Sep 17 00:00:00 2001 From: rustmailer Date: Fri, 3 Oct 2025 21:20:04 +0800 Subject: [PATCH] feat: unify UID/MID and UIDs/MIDs into single `id`/`ids` fields --- protos/rustmailer.proto | 148 ++++++++---------- src/modules/bounce/parser.rs | 1 - src/modules/cache/imap/sync/flow.rs | 3 +- .../cache/vendor/gmail/sync/history.rs | 23 +-- src/modules/database/manager.rs | 2 +- src/modules/grpc/service/message/from.rs | 15 +- src/modules/grpc/service/message/mod.rs | 9 +- src/modules/grpc/service/send/from.rs | 6 +- src/modules/grpc/tests.rs | 6 +- src/modules/message/append.rs | 64 +++----- src/modules/message/attachment.rs | 50 +++--- src/modules/message/content.rs | 54 ++----- src/modules/message/delete.rs | 45 +++--- src/modules/message/full.rs | 17 +- src/modules/message/transfer.rs | 49 +++--- src/modules/rest/api/message.rs | 14 +- src/modules/smtp/request/forward.rs | 26 +-- src/modules/smtp/request/mod.rs | 6 +- src/modules/smtp/request/reply.rs | 26 +-- .../components/mail-display-drawer.tsx | 8 +- 20 files changed, 231 insertions(+), 341 deletions(-) diff --git a/protos/rustmailer.proto b/protos/rustmailer.proto index e00b511..de0dd0e 100644 --- a/protos/rustmailer.proto +++ b/protos/rustmailer.proto @@ -566,35 +566,35 @@ service MailboxService { message MailboxTransferRequest { // The ID of the account. uint64 account_id = 1; - // A list of unique identifiers (UIDs) of the messages to transfer (IMAP accounts only). - // Ignored for Gmail API accounts. - repeated uint32 uids = 2; - // A list of Gmail API message IDs of the messages to transfer (Gmail API accounts only). - // Ignored for IMAP accounts. - repeated string mids = 3; + // A list of unique message identifiers as strings. + // - For IMAP accounts, each UID is converted to a numeric string (parseable back to `u32`). + // - For Gmail API accounts, each element is a message ID (`mid`) returned by the API. + // Unifying them as strings simplifies handling across different backends. + repeated string ids = 2; // The name of the mailbox or label from which to transfer messages. // For IMAP: decoded human-readable mailbox name (e.g., "INBOX"). // For Gmail API: the label name. - string current_mailbox = 4; + string current_mailbox = 3; // The name of the target mailbox or label to which messages will be transferred. // For IMAP: decoded human-readable mailbox name (e.g., "INBOX"). // For Gmail API: the label name. - string target_mailbox = 5; + string target_mailbox = 4; } // MessageDeleteRequest is used to delete messages from a mailbox. message MessageDeleteRequest { // The ID of the account. uint64 account_id = 1; - /// A list of unique identifiers (UIDs) of the messages to be deleted (IMAP only). - repeated uint32 uids = 2; - /// A list of Gmail message IDs of the messages to be deleted (Gmail API only). - repeated string mids = 3; + // A list of unique message identifiers as strings. + // - For IMAP accounts, each UID is converted to a numeric string (parseable back to `u32`). + // - For Gmail API accounts, each element is a message ID (`mid`) returned by the API. + // Unifying them as strings simplifies handling across different backends. + repeated string ids = 2; /// The decoded, human-readable name of the mailbox containing the email (e.g., "INBOX"). (IMAP only) /// 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. /// In Gmail API, this field is not required and can be set to `None`. - optional string mailbox_name = 4; + optional string mailbox_name = 3; } // FlagMessageRequest is used to update flags on messages within a mailbox. @@ -781,39 +781,37 @@ message FetchMessageContentRequest { uint64 account_id = 1; // The name of the mailbox containing the message. optional string mailbox_name = 2; - // The unique identifier (UID) of the message. - optional uint32 uid = 3; + // 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. + string id = 3; // Optional: The maximum length of content to fetch. optional uint64 max_length = 4; // A list of specific body sections to fetch. repeated EmailBodyPart sections = 5; // A list of inline attachments to fetch. repeated ImapAttachment inline = 6; - // The Gmail API message ID (Gmail API accounts only) - // This is the `id` returned by `list messages` and used by `get message`. - // For IMAP accounts, this field is ignored. - optional string mid = 7; } // FetchMessageAttachmentRequest is used to fetch a specific attachment from an email message. message FetchMessageAttachmentRequest { // The ID of the account. uint64 account_id = 1; - // The unique identifier (UID) of the message. - optional uint32 uid = 2; + // 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. + string id = 2; // The name of the mailbox containing the message. optional string mailbox_name = 3; // The attachment to fetch, identified by its data. optional ImapAttachment attachment = 4; - // The message identifier string (Gmail API `id`) - // - Required for Gmail API accounts - // - Not used for IMAP/SMTP - optional string mid = 5; // Gmail API only: attachment info used to fetch it via Gmail API. - optional AttachmentInfo attachment_info = 6; + optional AttachmentInfo attachment_info = 5; // Optional: The filename of the attachment. // - Gmail API only. - optional string filename = 7; + optional string filename = 6; } // FetchRawMessageRequest is used to fetch the complete raw content of an email message. @@ -822,10 +820,11 @@ message FetchRawMessageRequest { uint64 account_id = 1; // The name of the mailbox containing the message. (IMAP only) optional string mailbox_name = 2; - // The unique identifier (UID) of the message. (IMAP only) - optional uint32 uid = 3; - // The Gmail message ID of the email to fetch. (Gmail API only) - optional string mid = 4; + // 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. + string id = 3; } // MessageSearch represents a search query for email messages, which can be a single condition or a logical combination of conditions. @@ -1121,10 +1120,11 @@ message AppendReplyToDraftRequest { // - For IMAP accounts, this is the mailbox name where the source message resides. // - For Gmail API accounts, this refers to the label name associated with the source message. string mailbox_name = 2; - // The UID of the message being replied to (IMAP accounts only) - // For IMAP accounts, this identifies the specific message in the mailbox. - // For Gmail API accounts, this field is ignored. - optional uint32 uid = 3; + // 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. + string id = 3; // Optional preview text for the reply email optional string preview = 4; // Optional plain text body of the reply email @@ -1135,10 +1135,6 @@ message AppendReplyToDraftRequest { // For example: "[Gmail]/Drafts". // For Gmail API accounts, this field is ignored. optional string draft_folder_path = 7; - // The Gmail API message ID (Gmail API accounts only) - // This is the `id` returned by `list messages` and used by `get message`. - // For IMAP accounts, this field is ignored. - optional string mid = 8; } // MessageService provides APIs for interacting with email messages. @@ -1817,76 +1813,70 @@ message SendEmailRequest { message ReplyEmailRequest { // The name of the mailbox containing the original message. string mailbox_name = 1; - // The UID of the message being replied to. - // This identifies the specific message in the mailbox for **IMAP accounts**. - // Should be `None` when using Gmail API accounts. - optional uint32 uid = 2; - // The message ID of the message being replied to. - // This is used for **Gmail API accounts** instead of IMAP UID. - // Should be `None` when using IMAP accounts. - optional string mid = 3; + // 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. + string id = 2; // Optional: The plain text content of the reply. - optional string text = 4; + optional string text = 3; // Optional: The HTML content of the reply. - optional string html = 5; + optional string html = 4; // Optional: A preview text for the reply. - optional string preview = 6; + optional string preview = 5; // A map of custom headers for the reply. - map headers = 7; + map headers = 6; // If true, replies to all recipients (To, CC) of the original email. - bool reply_all = 8; + bool reply_all = 7; // A list of attachments to include in the reply. - repeated MailAttachment attachments = 9; + repeated MailAttachment attachments = 8; // A list of additional CC recipients for the reply. - repeated EmailAddress cc = 10; + repeated EmailAddress cc = 9; // A list of additional BCC recipients for the reply. - repeated EmailAddress bcc = 11; + repeated EmailAddress bcc = 10; // Optional: The timezone for date headers in the reply. - optional string timezone = 12; + optional string timezone = 11; // If true, includes the original message content in the reply. - bool include_original = 13; + bool include_original = 12; // If true, includes all attachments from the original message in the reply. - bool include_all_attachments = 14; + bool include_all_attachments = 13; // Controls the sending process, including retry policies and DSN. - SendControl send_control = 15; + SendControl send_control = 14; } // ForwardEmailRequest defines the parameters for forwarding an existing email. message ForwardEmailRequest { // The name of the mailbox containing the original message. string mailbox_name = 1; - // The UID of the message being replied to. - // This identifies the specific message in the mailbox for **IMAP accounts**. - // Should be `None` when using Gmail API accounts. - optional uint32 uid = 2; - // The message ID of the message being replied to. - // This is used for **Gmail API accounts** instead of IMAP UID. - // Should be `None` when using IMAP accounts. - optional string mid = 3; + // 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. + string id = 2; // A list of primary recipients (To) for the forwarded email. - repeated EmailAddress to = 4; + repeated EmailAddress to = 3; // A list of carbon copy (CC) recipients for the forwarded email. - repeated EmailAddress cc = 5; + repeated EmailAddress cc = 4; // A list of blind carbon copy (BCC) recipients for the forwarded email. - repeated EmailAddress bcc = 6; + repeated EmailAddress bcc = 5; // Optional: The plain text content to add to the forwarded email. - optional string text = 7; + optional string text = 6; // Optional: The HTML content to add to the forwarded email. - optional string html = 8; + optional string html = 7; // Optional: A preview text for the forwarded email. - optional string preview = 9; + optional string preview = 8; // A map of custom headers for the forwarded email. - map headers = 10; + map headers = 9; // Optional: The timezone for date headers in the forwarded email. - optional string timezone = 11; + optional string timezone = 10; // A list of attachments to include in the forwarded email. - repeated MailAttachment attachments = 12; + repeated MailAttachment attachments = 11; // If true, includes the original message content in the forwarded email. - bool include_original = 13; + bool include_original = 12; // If true, includes all attachments from the original message in the forwarded email. - bool include_all_attachments = 14; + bool include_all_attachments = 13; // Controls the sending process, including retry policies and DSN. - SendControl send_control = 15; + SendControl send_control = 14; } // EmailTask represents a single email sending task managed by the system. diff --git a/src/modules/bounce/parser.rs b/src/modules/bounce/parser.rs index f202c79..fa216e8 100644 --- a/src/modules/bounce/parser.rs +++ b/src/modules/bounce/parser.rs @@ -246,7 +246,6 @@ fn parse_original_message_headers(message: &Message<'_>) -> Option { let request = MessageContentRequest { mailbox: Some(remote.name.clone()), - uid: Some(envelope.uid), + id: envelope.uid.to_string(), max_length: Some(SETTINGS.rustmailer_max_email_content_length as usize), sections: Some(sections), inline: envelope .attachments .as_ref() .map(|att| att.iter().filter(|a| a.inline).cloned().collect()), - mid: None, }; retrieve_email_content(account.id, request, true).await? } diff --git a/src/modules/cache/vendor/gmail/sync/history.rs b/src/modules/cache/vendor/gmail/sync/history.rs index 4f88469..6252152 100644 --- a/src/modules/cache/vendor/gmail/sync/history.rs +++ b/src/modules/cache/vendor/gmail/sync/history.rs @@ -243,16 +243,13 @@ pub async fn apply_history( if !item.message.label_ids.contains(&label.label_id) { return None; } - let message_data = match GmailClient::get_message( - account_id, - use_proxy.clone(), - &item.message.id, - ) - .await - { - Ok(msg) => msg, - Err(_) => return None, - }; + let message_data = + match GmailClient::get_message(account_id, use_proxy.clone(), &item.message.id) + .await + { + Ok(msg) => msg, + Err(_) => return None, + }; if !message_data.label_ids.contains(&label.label_id) { return None; } @@ -277,6 +274,12 @@ pub async fn apply_history( } // save to local envelope cache and build some index if !messages_added.is_empty() { + info!( + "Gmail Api Account {} synced {} new messages in label '{}'", + account.id, + messages_added.len(), + &label.name + ); GmailEnvelope::save_envelopes(messages_added.clone()).await?; if EventHookTask::is_watching_email_add_event(account.id).await? { dispatch_new_email_notification(account, messages_added).await?; diff --git a/src/modules/database/manager.rs b/src/modules/database/manager.rs index d6dbcce..2590129 100644 --- a/src/modules/database/manager.rs +++ b/src/modules/database/manager.rs @@ -34,8 +34,8 @@ use crate::modules::{ token::AccessToken, }; +/// Metadata database instance pub struct DatabaseManager { - /// Metadata database instance meta_db: Arc>, /// Task scheduling database instance tasks_db: Arc>, diff --git a/src/modules/grpc/service/message/from.rs b/src/modules/grpc/service/message/from.rs index e960ac1..c1e92d7 100644 --- a/src/modules/grpc/service/message/from.rs +++ b/src/modules/grpc/service/message/from.rs @@ -29,10 +29,9 @@ use crate::modules::{ impl From for MailboxTransferRequest { fn from(value: rustmailer_grpc::MailboxTransferRequest) -> Self { Self { - uids: Some(value.uids).filter(|v| !v.is_empty()), + ids: value.ids, current_mailbox: value.current_mailbox, target_mailbox: value.target_mailbox, - mids: Some(value.mids).filter(|v| !v.is_empty()), } } } @@ -40,9 +39,8 @@ impl From for MailboxTransferRequest { impl From for MessageDeleteRequest { fn from(value: rustmailer_grpc::MessageDeleteRequest) -> Self { Self { - uids: Some(value.uids).filter(|v| !v.is_empty()), + ids: value.ids, mailbox: value.mailbox_name, - mids: Some(value.mids).filter(|v| !v.is_empty()), } } } @@ -389,7 +387,7 @@ impl TryFrom for MessageContentRequ fn try_from(value: rustmailer_grpc::FetchMessageContentRequest) -> Result { Ok(Self { mailbox: value.mailbox_name, - uid: value.uid, + id: value.id, max_length: value.max_length.map(|m| m as usize), sections: (!value.sections.is_empty()) .then(|| { @@ -409,7 +407,6 @@ impl TryFrom for MessageContentRequ .collect::, _>>() }) .transpose()?, - mid: value.mid, }) } } @@ -473,10 +470,9 @@ impl TryFrom for AttachmentReque value: rustmailer_grpc::FetchMessageAttachmentRequest, ) -> Result { Ok(Self { - uid: value.uid, + id: value.id, mailbox: value.mailbox_name, attachment: value.attachment.map(|a| a.try_into()).transpose()?, - mid: value.mid, attachment_info: value.attachment_info.map(|a| a.into()), filename: value.filename, }) @@ -609,12 +605,11 @@ impl From for AppendReplyToDraftRequ fn from(value: rustmailer_grpc::AppendReplyToDraftRequest) -> Self { Self { mailbox_name: value.mailbox_name, - uid: value.uid, + id: value.id, preview: value.preview, text: value.text, html: value.html, draft_folder_path: value.draft_folder_path, - mid: value.mid, } } } diff --git a/src/modules/grpc/service/message/mod.rs b/src/modules/grpc/service/message/mod.rs index 5072656..66df1ac 100644 --- a/src/modules/grpc/service/message/mod.rs +++ b/src/modules/grpc/service/message/mod.rs @@ -143,13 +143,8 @@ impl MessageService for RustMailerMessageService { request: Request, ) -> Result, Status> { let req = require_account_access(request, |r| r.account_id)?; - let mut reader = retrieve_raw_email( - req.account_id, - req.mailbox_name.as_deref(), - req.uid, - req.mid.as_deref(), - ) - .await?; + let mut reader = + retrieve_raw_email(req.account_id, req.mailbox_name.as_deref(), &req.id).await?; let mut buffer = Vec::new(); reader diff --git a/src/modules/grpc/service/send/from.rs b/src/modules/grpc/service/send/from.rs index ff1e41c..1a60274 100644 --- a/src/modules/grpc/service/send/from.rs +++ b/src/modules/grpc/service/send/from.rs @@ -65,7 +65,7 @@ impl TryFrom for ReplyEmailRequest { fn try_from(value: rustmailer_grpc::ReplyEmailRequest) -> Result { Ok(Self { mailbox_name: value.mailbox_name, - uid: value.uid, + id: value.id, text: value.text, html: value.html, preview: value.preview, @@ -98,7 +98,6 @@ impl TryFrom for ReplyEmailRequest { include_original: value.include_original, include_all_attachments: value.include_all_attachments, send_control: { value.send_control.map(|c| c.try_into()).transpose()? }, - mid: value.mid, }) } } @@ -109,7 +108,7 @@ impl TryFrom for ForwardEmailRequest { fn try_from(value: rustmailer_grpc::ForwardEmailRequest) -> Result { Ok(Self { mailbox_name: value.mailbox_name, - uid: value.uid, + id: value.id, to: value.to.into_iter().map(Into::into).collect(), cc: (!value.cc.is_empty()).then(|| value.cc.into_iter().map(Into::into).collect()), bcc: (!value.bcc.is_empty()).then(|| value.bcc.into_iter().map(Into::into).collect()), @@ -141,7 +140,6 @@ impl TryFrom for ForwardEmailRequest { include_original: value.include_original, include_all_attachments: value.include_all_attachments, send_control: { value.send_control.map(|c| c.try_into()).transpose()? }, - mid: value.mid, }) } } diff --git a/src/modules/grpc/tests.rs b/src/modules/grpc/tests.rs index 6c1dd80..0ff2ce9 100644 --- a/src/modules/grpc/tests.rs +++ b/src/modules/grpc/tests.rs @@ -186,12 +186,11 @@ async fn test6() { let request = AppendReplyToDraftRequest { account_id: 6637484689546669, mailbox_name: "INBOX".into(), - uid: Some(395), + id: "395".into(), preview: None, text: Some("hello world.".into()), html: None, draft_folder_path: Some("[Gmail]/Drafts".into()), - mid: None, }; let mut request = poem_grpc::Request::new(request); @@ -247,12 +246,11 @@ async fn test8() { let request = AppendReplyToDraftRequest { account_id: 4391092875701825, mailbox_name: "INBOX".into(), - uid: None, preview: None, text: Some("hello world.".into()), html: None, draft_folder_path: None, - mid: Some("1970d297da3c2dd2".into()), + id: "1970d297da3c2dd2".into(), }; let mut request = poem_grpc::Request::new(request); diff --git a/src/modules/message/append.rs b/src/modules/message/append.rs index 9ecb4fa..d1cb882 100644 --- a/src/modules/message/append.rs +++ b/src/modules/message/append.rs @@ -37,16 +37,12 @@ pub struct AppendReplyToDraftRequest { /// - For Gmail API accounts, this refers to the label name associated with the source message. /// This is used to locate the message being replied to. pub mailbox_name: String, - /// The UID of the message being replied to (IMAP accounts only). + /// The unique ID of the message, either IMAP UID or Gmail API MID. /// - /// For IMAP accounts, this identifies the specific message in the mailbox. - /// For Gmail API accounts, this field is ignored. - pub uid: Option, - /// The Gmail API message ID (Gmail API accounts only), sourced from [`EmailEnvelopeV3::mid`]. - /// - /// This is the `id` returned by `list messages` and used by `get message`. - /// For IMAP accounts, this field is ignored. - pub mid: Option, + /// - 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. + pub id: String, /// A preview text for the reply email. /// /// This optional field provides a short summary or preview of the reply content. @@ -79,19 +75,11 @@ impl AppendReplyToDraftRequest { )); } - if is_gmail_api { - // Gmail API account: mid required - if self.mid.as_ref().map(|s| s.is_empty()).unwrap_or(true) { - return Err(raise_error!( - "mid is required for Gmail API accounts".into(), - ErrorCode::InvalidParameter - )); - } - } else { + if !is_gmail_api { // IMAP account: uid and draft_folder_path required - if self.uid.is_none() { + if self.id.parse::().is_err() { return Err(raise_error!( - "uid is required for IMAP accounts".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter )); } @@ -129,12 +117,7 @@ impl AppendReplyToDraftRequest { let envelope = EmailHandler::get_envelope( account, &self.mailbox_name, - self.uid.ok_or_else(|| { - raise_error!( - "uid is missing but required for IMAP accounts".into(), - ErrorCode::InternalError - ) - })?, + self.id.parse::().ok().unwrap(), ) .await?; @@ -187,28 +170,17 @@ impl AppendReplyToDraftRequest { account_id: u64, ) -> RustMailerResult<()> { let target_label = GmailLabels::get_by_name(account_id, &self.mailbox_name).await?; - let envelope = GmailEnvelope::find( - account_id, - target_label.id, - &self.mid.as_ref().ok_or_else(|| { + let envelope = GmailEnvelope::find(account_id, target_label.id, &self.id) + .await? + .ok_or_else(|| { raise_error!( - "mid is missing but required for Gmail API accounts".into(), - ErrorCode::InternalError + format!( + "Gmail message with id '{}' not found in label '{}' for account {}", + self.id, target_label.name, account_id + ), + ErrorCode::ResourceNotFound ) - })?, - ) - .await? - .ok_or_else(|| { - raise_error!( - format!( - "Gmail message with id '{}' not found in label '{}' for account {}", - self.mid.as_ref().unwrap(), - target_label.name, - account_id - ), - ErrorCode::ResourceNotFound - ) - })?; + })?; let from = Address::new_address( account.name.as_ref().map(|n| Cow::Owned(n.to_string())), diff --git a/src/modules/message/attachment.rs b/src/modules/message/attachment.rs index e6f9943..bdcd592 100644 --- a/src/modules/message/attachment.rs +++ b/src/modules/message/attachment.rs @@ -26,19 +26,18 @@ const MAX_ATTACHMENT_SIZE: usize = 52_428_800; // 50MB /// Represents a request to fetch an attachment from a message in a mailbox. #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] pub struct AttachmentRequest { - /// IMAP only: The UID of the message containing the attachment. - /// Not used for Gmail API accounts. - pub uid: Option, /// IMAP only: The name of the mailbox where the message is located. /// Not used for Gmail API accounts. pub mailbox: Option, /// IMAP only: The metadata describing the attachment to fetch. /// Not used for Gmail API accounts. pub attachment: Option, - /// The message identifier string (Gmail API `id`) - /// - Required for Gmail API accounts - /// - Not used for IMAP/SMTP - pub mid: Option, + /// 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. + pub id: String, /// Gmail API only: attachment info used to fetch it via Gmail API. /// Not used for IMAP accounts. pub attachment_info: Option, @@ -51,13 +50,19 @@ impl AttachmentRequest { pub fn validate(&self, account: &AccountV2) -> RustMailerResult<()> { match account.mailer_type { MailerType::ImapSmtp => { - if self.uid.is_none() || self.mailbox.is_none() || self.attachment.is_none() { + if self.mailbox.is_none() || self.attachment.is_none() { return Err(raise_error!( - format!( - "Current account type is `ImapSmtp`. Downloading attachments requires `uid`, `mailbox`, and `attachment` metadata." - ), - ErrorCode::InvalidParameter - )); + format!( + "Current account type is `ImapSmtp`. Downloading attachments requires `uid`, `mailbox`, and `attachment` metadata." + ), + ErrorCode::InvalidParameter + )); + } + if self.id.parse::().is_err() { + return Err(raise_error!( + "Invalid IMAP UID: `id` must be a numeric string".into(), + ErrorCode::InvalidParameter + )); } } MailerType::GmailApi => { @@ -67,12 +72,6 @@ impl AttachmentRequest { ErrorCode::InvalidParameter )); } - if self.mid.is_none() { - return Err(raise_error!( - "Current account type is `Gmail API`. Downloading attachments requires `mid`.".into(), - ErrorCode::InvalidParameter - )); - } } } Ok(()) @@ -151,9 +150,9 @@ pub async fn retrieve_email_attachment( ErrorCode::InvalidParameter ) })?; - let uid = request.uid.ok_or_else(|| { + let uid = request.id.parse::().ok().ok_or_else(|| { raise_error!( - "`uid` is required when retrieving attachments for IMAP/SMTP accounts.".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter ) })?; @@ -162,13 +161,6 @@ pub async fn retrieve_email_attachment( Ok((reader, filename)) } MailerType::GmailApi => { - let mid = request.mid.as_ref().ok_or_else(|| { - raise_error!( - "`mid` is required when retrieving attachments for Gmail API accounts.".into(), - ErrorCode::InvalidParameter - ) - })?; - let attachment_info = request.attachment_info.as_ref().ok_or_else(|| { raise_error!( "`attachment_info` is required when retrieving attachments for Gmail API accounts.".into(), @@ -176,7 +168,7 @@ pub async fn retrieve_email_attachment( ) })?; let filename = request.filename; - let reader = retrieve_gmail_attachment(&account, &mid, &attachment_info).await?; + let reader = retrieve_gmail_attachment(&account, &request.id, &attachment_info).await?; Ok((reader, filename)) } } diff --git a/src/modules/message/content.rs b/src/modules/message/content.rs index 71e13a3..9113462 100644 --- a/src/modules/message/content.rs +++ b/src/modules/message/content.rs @@ -36,14 +36,12 @@ pub struct MessageContentRequest { /// - Required for IMAP/SMTP accounts /// - Not used for Gmail API pub mailbox: Option, - /// The unique identifier of the message within the mailbox (IMAP UID) - /// - Required for IMAP/SMTP accounts - /// - Not used for Gmail API - pub uid: Option, - /// The message identifier string (Gmail API `id`) - /// - Required for Gmail API accounts - /// - Not used for IMAP/SMTP - pub mid: Option, + /// 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. + pub id: String, /// Optional maximum length to retrieve for text parts (useful for large messages) /// - Supported by both IMAP/SMTP and Gmail API pub max_length: Option, @@ -67,9 +65,9 @@ impl MessageContentRequest { ErrorCode::InvalidParameter )); } - if self.uid.is_none() { + if self.id.parse::().is_err() { return Err(raise_error!( - "`uid` is required for IMAP/SMTP accounts.".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter )); } @@ -79,32 +77,14 @@ impl MessageContentRequest { ErrorCode::InvalidParameter )); } - if self.mid.is_some() { - return Err(raise_error!( - "`mid` must not be set for IMAP/SMTP accounts.".into(), - ErrorCode::InvalidParameter - )); - } } MailerType::GmailApi => { - if self.mid.is_none() { - return Err(raise_error!( - "`mid` is required for Gmail API accounts.".into(), - ErrorCode::InvalidParameter - )); - } if self.mailbox.is_some() { return Err(raise_error!( "`mailbox` must not be set for Gmail API accounts.".into(), ErrorCode::InvalidParameter )); } - if self.uid.is_some() { - return Err(raise_error!( - "`uid` must not be set for Gmail API accounts.".into(), - ErrorCode::InvalidParameter - )); - } if self.sections.is_some() { return Err(raise_error!( "`sections` is only supported for IMAP/SMTP accounts.".into(), @@ -415,9 +395,9 @@ pub async fn retrieve_email_content( ErrorCode::InvalidParameter ) })?; - let uid = request.uid.ok_or_else(|| { + let uid = request.id.parse::().ok().ok_or_else(|| { raise_error!( - "`uid` is required when retrieving IMAP/SMTP message content.".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter ) })?; @@ -440,18 +420,8 @@ pub async fn retrieve_email_content( .await } MailerType::GmailApi => { - retrieve_gmail_message_content( - account_id, - request.mid.ok_or_else(|| { - raise_error!( - "`mid` is required when retrieving Gmail API message content.".into(), - ErrorCode::InvalidParameter - ) - })?, - request.max_length, - skip_cache, - ) - .await + retrieve_gmail_message_content(account_id, request.id, request.max_length, skip_cache) + .await } } } diff --git a/src/modules/message/delete.rs b/src/modules/message/delete.rs index e709512..cacdd89 100644 --- a/src/modules/message/delete.rs +++ b/src/modules/message/delete.rs @@ -15,10 +15,12 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] pub struct MessageDeleteRequest { - /// A list of unique identifiers (UIDs) of the messages to be deleted (IMAP only). - pub uids: Option>, - /// A list of Gmail message IDs of the messages to be deleted (Gmail API only). - pub mids: Option>, + /// A list of unique message identifiers as strings. + /// + /// - For IMAP accounts, each UID is converted to a numeric string (parseable back to `u32`). + /// - For Gmail API accounts, each element is a message ID (`mid`) returned by the API. + /// Unifying them as strings simplifies handling across different backends. + pub ids: Vec, /// The decoded, human-readable name of the mailbox containing the email (e.g., "INBOX"). (IMAP only) /// 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. @@ -41,24 +43,29 @@ pub async fn move_to_trash( ) })?; - let uids = request.uids.as_deref().ok_or_else(|| { - raise_error!( - "IMAP request missing required field 'uids'".into(), + if request.ids.is_empty() { + return Err(raise_error!( + "`ids` must contain at least one element".into(), ErrorCode::InvalidParameter - ) - })?; + )); + } - move_to_trash_or_delete_messages_directly(account_id, uids, mailbox).await - } - MailerType::GmailApi => { - let mids = request.mids.as_deref().ok_or_else(|| { - raise_error!( - "Gmail request missing required field 'mids'".into(), - ErrorCode::InvalidParameter - ) - })?; - gmail_move_to_trash(&account, mids).await + let uids: Vec = request + .ids + .iter() + .map(|id| { + id.parse::().map_err(|_| { + raise_error!( + format!("Invalid IMAP UID: '{}', must be a numeric string", id), + ErrorCode::InvalidParameter + ) + }) + }) + .collect::>()?; + + move_to_trash_or_delete_messages_directly(account_id, &uids, mailbox).await } + MailerType::GmailApi => gmail_move_to_trash(&account, &request.ids).await, } } diff --git a/src/modules/message/full.rs b/src/modules/message/full.rs index e0c991c..2e7315d 100644 --- a/src/modules/message/full.rs +++ b/src/modules/message/full.rs @@ -38,8 +38,7 @@ fn gmail_raw_email_diskcache_key(account_id: u64, mid: &str) -> String { pub async fn retrieve_raw_email( account_id: u64, mailbox: Option<&str>, - uid: Option, - mid: Option<&str>, + id: &str, ) -> RustMailerResult { let account = AccountV2::check_account_active(account_id, false).await?; match account.mailer_type { @@ -50,23 +49,15 @@ pub async fn retrieve_raw_email( ErrorCode::InvalidParameter ) })?; - let uid = uid.ok_or_else(|| { + let uid = id.parse::().ok().ok_or_else(|| { raise_error!( - "Missing required parameter: `uid` for IMAP/SMTP".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter ) })?; retrieve_imap_raw_email(account_id, mailbox, uid).await } - MailerType::GmailApi => { - let mid = mid.ok_or_else(|| { - raise_error!( - "Missing required parameter: `mid` for Gmail API".into(), - ErrorCode::InvalidParameter - ) - })?; - retrieve_gmail_raw_email(&account, mid).await - } + MailerType::GmailApi => retrieve_gmail_raw_email(&account, id).await, } } diff --git a/src/modules/message/transfer.rs b/src/modules/message/transfer.rs index 93f8cf1..1e78415 100644 --- a/src/modules/message/transfer.rs +++ b/src/modules/message/transfer.rs @@ -18,12 +18,12 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] pub struct MailboxTransferRequest { - /// A list of unique identifiers (UIDs) for the messages to be moved (IMAP accounts only). - /// For Gmail API accounts, this field is ignored. - pub uids: Option>, - /// A list of Gmail API message IDs for the messages to be moved (Gmail API accounts only). - /// For IMAP accounts, this field is ignored. - pub mids: Option>, + /// A list of unique message identifiers as strings. + /// + /// - For IMAP accounts, each UID is converted to a numeric string (parseable back to `u32`). + /// - For Gmail API accounts, each element is a message ID (`mid`) returned by the API. + /// Unifying them as strings simplifies handling across different backends. + pub ids: Vec, /// The name of the mailbox from which the messages will be moved. /// For IMAP: the decoded, human-readable name of the mailbox (e.g., "INBOX"). /// For Gmail API: represents the label name. @@ -51,19 +51,27 @@ pub async fn transfer_messages( match account.mailer_type { MailerType::ImapSmtp => { - let uids = payload.uids.as_ref().ok_or_else(|| { - raise_error!( - "IMAP copy requires `uids`, but none were provided".into(), - ErrorCode::InvalidParameter - ) - })?; - if uids.is_empty() { + if payload.ids.is_empty() { return Err(raise_error!( - "IMAP copy requires at least one UID".into(), + "`ids` must contain at least one element".into(), ErrorCode::InvalidParameter )); } - let uid_set = generate_uid_set(uids.clone()); + + let uids: Vec = payload + .ids + .iter() + .map(|id| { + id.parse::().map_err(|_| { + raise_error!( + format!("Invalid IMAP UID: '{}', must be a numeric string", id), + ErrorCode::InvalidParameter + ) + }) + }) + .collect::>()?; + + let uid_set = generate_uid_set(uids); let executor = RUST_MAIL_CONTEXT.imap(account_id).await?; // Encode the mailbox names using UTF-7 encoding let current_mailbox = encode_mailbox_name!(payload.current_mailbox.clone()); @@ -93,12 +101,7 @@ pub async fn transfer_messages( } } MailerType::GmailApi => { - let mids = payload.mids.as_ref().ok_or_else(|| { - raise_error!( - "Gmail API copy requires `mids`, but none were provided".into(), - ErrorCode::InvalidParameter - ) - })?; + let mids = &payload.ids; if mids.is_empty() { return Err(raise_error!( @@ -107,10 +110,10 @@ pub async fn transfer_messages( )); } - if mids.len() > 1000 { + if mids.len() > 500 { return Err(raise_error!( format!( - "Gmail API batchModify supports at most 1000 message IDs, got {}", + "Gmail API batchModify supports at most 500 message IDs, got {}", mids.len() ), ErrorCode::InvalidParameter diff --git a/src/modules/rest/api/message.rs b/src/modules/rest/api/message.rs index cb54da1..8a2747c 100644 --- a/src/modules/rest/api/message.rs +++ b/src/modules/rest/api/message.rs @@ -269,10 +269,11 @@ impl MessageApi { /// 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>, - /// The Gmail message ID of the email to fetch. - mid: 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, @@ -281,13 +282,12 @@ impl MessageApi { 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 mid_opt = mid.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(), - uid.0, - mid_opt.as_deref(), + id ) .await?; let body = Body::from_async_read(reader); diff --git a/src/modules/smtp/request/forward.rs b/src/modules/smtp/request/forward.rs index 8af4373..8377b86 100644 --- a/src/modules/smtp/request/forward.rs +++ b/src/modules/smtp/request/forward.rs @@ -36,16 +36,12 @@ pub struct ForwardEmailRequest { /// /// This is used to locate the source message that is being forwarded. pub mailbox_name: String, - /// The UID of the message being forwarded to. + /// The unique ID of the message, either IMAP UID or Gmail API MID. /// - /// This identifies the specific message in the mailbox for **IMAP accounts**. - /// Should be `None` when using Gmail API accounts. - pub uid: Option, - /// The message ID of the message being forwarded to. - /// - /// This is used for **Gmail API accounts** instead of IMAP UID. - /// Should be `None` when using IMAP accounts. - pub mid: Option, + /// - 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. + pub id: String, /// The list of primary recipients to forward the email to. /// /// At least one recipient must be specified. @@ -163,9 +159,9 @@ impl EmailBuilder for ForwardEmailRequest { let (envelope, answer_email) = match account.mailer_type { MailerType::ImapSmtp => { - let uid = self.uid.ok_or_else(|| { + let uid = self.id.parse::().ok().ok_or_else(|| { raise_error!( - "Missing required field `uid` for IMAP account".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter ) })?; @@ -180,14 +176,8 @@ impl EmailBuilder for ForwardEmailRequest { ) } MailerType::GmailApi => { - let mid = self.mid.as_ref().ok_or_else(|| { - raise_error!( - "Missing required field `mid` for Gmail API account".into(), - ErrorCode::InvalidParameter - ) - })?; let envelope = - EmailHandler::get_gmail_envelope(account, &self.mailbox_name, mid).await?; + EmailHandler::get_gmail_envelope(account, &self.mailbox_name, &self.id).await?; (envelope, None) } }; diff --git a/src/modules/smtp/request/mod.rs b/src/modules/smtp/request/mod.rs index dd0dac6..1917c72 100644 --- a/src/modules/smtp/request/mod.rs +++ b/src/modules/smtp/request/mod.rs @@ -212,10 +212,9 @@ impl MailAttachment { let (mut reader, _) = retrieve_email_attachment( account.id, AttachmentRequest { - uid: Some(attachment_ref.uid), + id: attachment_ref.uid.to_string(), mailbox: Some(attachment_ref.mailbox_name.clone()), attachment: Some(attachment_ref.attachment_data.clone()), - mid: None, attachment_info: None, filename: None, }, @@ -621,11 +620,10 @@ impl EmailHandler { }); let request = MessageContentRequest { mailbox: Some(envelope.mailbox_name.clone()), - uid: Some(envelope.uid), + id: envelope.uid.to_string(), max_length: None, sections: Some(body_meta.clone()), inline: inline_attachments, - mid: None, }; retrieve_email_content(account.id, request, false) .await diff --git a/src/modules/smtp/request/reply.rs b/src/modules/smtp/request/reply.rs index 461d691..b94830e 100644 --- a/src/modules/smtp/request/reply.rs +++ b/src/modules/smtp/request/reply.rs @@ -32,16 +32,12 @@ pub struct ReplyEmailRequest { /// /// This is used to locate the source message that is being replied to. pub mailbox_name: String, - /// The UID of the message being replied to. + /// The unique ID of the message, either IMAP UID or Gmail API MID. /// - /// This identifies the specific message in the mailbox for **IMAP accounts**. - /// Should be `None` when using Gmail API accounts. - pub uid: Option, - /// The message ID of the message being replied to. - /// - /// This is used for **Gmail API accounts** instead of IMAP UID. - /// Should be `None` when using IMAP accounts. - pub mid: Option, + /// - 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. + pub id: String, /// The plain text body of the reply email. /// /// This field is optional and can be used to provide plain text content. @@ -140,9 +136,9 @@ impl EmailBuilder for ReplyEmailRequest { let (envelope, answer_email) = match account.mailer_type { MailerType::ImapSmtp => { - let uid = self.uid.ok_or_else(|| { + let uid = self.id.parse::().ok().ok_or_else(|| { raise_error!( - "Missing required field `uid` for IMAP account".into(), + "Invalid IMAP UID: `id` must be a numeric string".into(), ErrorCode::InvalidParameter ) })?; @@ -157,14 +153,8 @@ impl EmailBuilder for ReplyEmailRequest { ) } MailerType::GmailApi => { - let mid = self.mid.as_ref().ok_or_else(|| { - raise_error!( - "Missing required field `mid` for Gmail API account".into(), - ErrorCode::InvalidParameter - ) - })?; let envelope = - EmailHandler::get_gmail_envelope(account, &self.mailbox_name, mid).await?; + EmailHandler::get_gmail_envelope(account, &self.mailbox_name, &self.id).await?; (envelope, None) } }; diff --git a/web/src/features/mailbox/components/mail-display-drawer.tsx b/web/src/features/mailbox/components/mail-display-drawer.tsx index 84165a3..049aa4f 100644 --- a/web/src/features/mailbox/components/mail-display-drawer.tsx +++ b/web/src/features/mailbox/components/mail-display-drawer.tsx @@ -163,9 +163,9 @@ export function MailDisplayDrawer({ open, setOpen, onOpenChange, currentEnvelope if (currentEnvelope.mid) { setLoading(true); let payload: { - mid: string; + id: string; } = { - mid: currentEnvelope.mid + id: currentEnvelope.mid }; loadMessageMutation.mutate({ accountId: currentAccountId!, payload }) } else { @@ -191,12 +191,12 @@ export function MailDisplayDrawer({ open, setOpen, onOpenChange, currentEnvelope inlineAttachments = currentEnvelope.attachments.filter(attachment => attachment.inline === true); } let payload: { - uid: number; + id: string; mailbox: string | undefined; sections: EmailBodyPart[]; inline?: Attachment[]; // Declare inline as an optional field } = { - uid: currentEnvelope.uid, + id: currentEnvelope.uid.toString(), mailbox: currentMailbox?.name, sections: [emailbody], };