feat: unify UID/MID and UIDs/MIDs into single id/ids fields

This commit is contained in:
rustmailer
2025-10-03 21:20:04 +08:00
parent e6225d13bd
commit 60041ee59f
20 changed files with 231 additions and 341 deletions
+69 -79
View File
@@ -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<string, HeaderValue> headers = 7;
map<string, HeaderValue> 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<string, HeaderValue> headers = 10;
map<string, HeaderValue> 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.
-1
View File
@@ -246,7 +246,6 @@ fn parse_original_message_headers(message: &Message<'_>) -> Option<RawEmailHeade
let part = rfc822_header_part?;
let sub_message = part.is_message().then(|| part.message()).flatten()?;
if sub_message.is_empty() {
return None;
}
+1 -2
View File
@@ -803,14 +803,13 @@ async fn process_email_added_events(
Some(sections) => {
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?
}
+13 -10
View File
@@ -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?;
+1 -1
View File
@@ -34,8 +34,8 @@ use crate::modules::{
token::AccessToken,
};
/// Metadata database instance
pub struct DatabaseManager {
/// Metadata database instance
meta_db: Arc<Database<'static>>,
/// Task scheduling database instance
tasks_db: Arc<Database<'static>>,
+5 -10
View File
@@ -29,10 +29,9 @@ use crate::modules::{
impl From<rustmailer_grpc::MailboxTransferRequest> 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<rustmailer_grpc::MailboxTransferRequest> for MailboxTransferRequest {
impl From<rustmailer_grpc::MessageDeleteRequest> 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<rustmailer_grpc::FetchMessageContentRequest> for MessageContentRequ
fn try_from(value: rustmailer_grpc::FetchMessageContentRequest) -> Result<Self, Self::Error> {
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<rustmailer_grpc::FetchMessageContentRequest> for MessageContentRequ
.collect::<Result<Vec<_>, _>>()
})
.transpose()?,
mid: value.mid,
})
}
}
@@ -473,10 +470,9 @@ impl TryFrom<rustmailer_grpc::FetchMessageAttachmentRequest> for AttachmentReque
value: rustmailer_grpc::FetchMessageAttachmentRequest,
) -> Result<Self, Self::Error> {
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<rustmailer_grpc::AppendReplyToDraftRequest> 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,
}
}
}
+2 -7
View File
@@ -143,13 +143,8 @@ impl MessageService for RustMailerMessageService {
request: Request<FetchRawMessageRequest>,
) -> Result<Response<ByteResponse>, 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
+2 -4
View File
@@ -65,7 +65,7 @@ impl TryFrom<rustmailer_grpc::ReplyEmailRequest> for ReplyEmailRequest {
fn try_from(value: rustmailer_grpc::ReplyEmailRequest) -> Result<Self, Self::Error> {
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<rustmailer_grpc::ReplyEmailRequest> 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<rustmailer_grpc::ForwardEmailRequest> for ForwardEmailRequest {
fn try_from(value: rustmailer_grpc::ForwardEmailRequest) -> Result<Self, Self::Error> {
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<rustmailer_grpc::ForwardEmailRequest> 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,
})
}
}
+2 -4
View File
@@ -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);
+18 -46
View File
@@ -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<u32>,
/// 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<String>,
/// - 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::<u32>().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::<u32>().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())),
+21 -29
View File
@@ -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<u32>,
/// IMAP only: The name of the mailbox where the message is located.
/// Not used for Gmail API accounts.
pub mailbox: Option<String>,
/// IMAP only: The metadata describing the attachment to fetch.
/// Not used for Gmail API accounts.
pub attachment: Option<ImapAttachment>,
/// The message identifier string (Gmail API `id`)
/// - Required for Gmail API accounts
/// - Not used for IMAP/SMTP
pub mid: Option<String>,
/// 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<AttachmentInfo>,
@@ -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::<u32>().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::<u32>().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))
}
}
+12 -42
View File
@@ -36,14 +36,12 @@ pub struct MessageContentRequest {
/// - Required for IMAP/SMTP accounts
/// - Not used for Gmail API
pub mailbox: Option<String>,
/// The unique identifier of the message within the mailbox (IMAP UID)
/// - Required for IMAP/SMTP accounts
/// - Not used for Gmail API
pub uid: Option<u32>,
/// The message identifier string (Gmail API `id`)
/// - Required for Gmail API accounts
/// - Not used for IMAP/SMTP
pub mid: Option<String>,
/// 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<usize>,
@@ -67,9 +65,9 @@ impl MessageContentRequest {
ErrorCode::InvalidParameter
));
}
if self.uid.is_none() {
if self.id.parse::<u32>().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::<u32>().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
}
}
}
+26 -19
View File
@@ -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<Vec<u32>>,
/// A list of Gmail message IDs of the messages to be deleted (Gmail API only).
pub mids: Option<Vec<String>>,
/// 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<String>,
/// 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<u32> = request
.ids
.iter()
.map(|id| {
id.parse::<u32>().map_err(|_| {
raise_error!(
format!("Invalid IMAP UID: '{}', must be a numeric string", id),
ErrorCode::InvalidParameter
)
})
})
.collect::<Result<_, _>>()?;
move_to_trash_or_delete_messages_directly(account_id, &uids, mailbox).await
}
MailerType::GmailApi => gmail_move_to_trash(&account, &request.ids).await,
}
}
+4 -13
View File
@@ -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<u32>,
mid: Option<&str>,
id: &str,
) -> RustMailerResult<cacache::Reader> {
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::<u32>().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,
}
}
+26 -23
View File
@@ -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<Vec<u32>>,
/// 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<Vec<String>>,
/// 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<String>,
/// 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<u32> = payload
.ids
.iter()
.map(|id| {
id.parse::<u32>().map_err(|_| {
raise_error!(
format!("Invalid IMAP UID: '{}', must be a numeric string", id),
ErrorCode::InvalidParameter
)
})
})
.collect::<Result<_, _>>()?;
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
+7 -7
View File
@@ -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<Option<String>>,
/// The IMAP UID of the email to fetch.
uid: Query<Option<u32>>,
/// The Gmail message ID of the email to fetch.
mid: Query<Option<String>>,
/// 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<String>,
/// An optional filename for the attachment (defaults to a timestamped `.elm` file).
filename: Query<Option<String>>,
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);
+8 -18
View File
@@ -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<u32>,
/// 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<String>,
/// - 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::<u32>().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)
}
};
+2 -4
View File
@@ -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
+8 -18
View File
@@ -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<u32>,
/// 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<String>,
/// - 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::<u32>().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)
}
};
@@ -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],
};