refactor: merge uid and mid into a single id field in EmailAddedToFolder and EmailEnvelopeV3

This commit is contained in:
rustmailer
2025-10-08 12:05:31 +08:00
parent 3ed4d0395c
commit bd6efb153c
17 changed files with 336 additions and 119 deletions
+1 -2
View File
@@ -829,7 +829,7 @@ async fn process_email_added_events(
account_id: account.id,
account_email: account.email.clone(),
mailbox_name: remote.name.clone(),
uid: envelope.uid,
id: envelope.uid.to_string(),
internal_date: envelope.internal_date,
date: envelope.date,
from: envelope.from,
@@ -850,7 +850,6 @@ async fn process_email_added_events(
.as_ref()
.map(|atts| atts.iter().cloned().map(Attachment::from).collect()),
thread_id,
mid: None,
labels: vec![],
}),
),
+8 -4
View File
@@ -14,6 +14,7 @@ use crate::{
account::v2::AccountV2,
cache::{
imap::v2::EmailEnvelopeV3,
model::Envelope,
vendor::gmail::sync::{client::GmailClient, envelope::GmailEnvelope},
},
database::{
@@ -142,7 +143,7 @@ impl EmailThread {
page: u64,
page_size: u64,
desc: bool,
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
) -> RustMailerResult<DataPage<Envelope>> {
let threads = paginate_secondary_scan_impl::<EmailThread>(
DB_MANAGER.envelope_db(),
Some(page),
@@ -172,7 +173,7 @@ impl EmailThread {
current_page: threads.page,
page_size: threads.page_size,
total_items: threads.total_items,
items: envelopes,
items: envelopes.into_iter().map(Envelope::from).collect(),
total_pages: threads.total_pages,
})
}
@@ -183,7 +184,7 @@ impl EmailThread {
page: u64,
page_size: u64,
desc: bool,
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
) -> RustMailerResult<DataPage<Envelope>> {
let threads = paginate_secondary_scan_impl::<EmailThread>(
DB_MANAGER.envelope_db(),
Some(page),
@@ -208,7 +209,10 @@ impl EmailThread {
let results: RustMailerResult<Vec<GmailEnvelope>> =
join_all(fetch_tasks).await.into_iter().collect();
let map = GmailClient::label_map(account.id, account.use_proxy).await?;
let envelopes = results?.into_iter().map(|e| e.into_v3(&map)).collect();
let envelopes = results?
.into_iter()
.map(|e| e.into_envelope(&map))
.collect();
Ok(DataPage {
current_page: threads.page,
page_size: threads.page_size,
+12 -12
View File
@@ -14,13 +14,16 @@ use tracing::{error, info};
use crate::{
calculate_hash, id,
modules::{
cache::imap::{
address::AddressEntity,
envelope::{EmailEnvelope, Received},
mailbox::EnvelopeFlag,
manager::EnvelopeFlagsManager,
minimal::MinimalEnvelope,
thread::{EmailThread, EmailThreadKey},
cache::{
imap::{
address::AddressEntity,
envelope::{EmailEnvelope, Received},
mailbox::EnvelopeFlag,
manager::EnvelopeFlagsManager,
minimal::MinimalEnvelope,
thread::{EmailThread, EmailThreadKey},
},
model::Envelope,
},
common::Addr,
database::{
@@ -281,10 +284,7 @@ impl EmailEnvelopeV3 {
.await
}
pub async fn get_thread(
account_id: u64,
thread_id: u64,
) -> RustMailerResult<Vec<EmailEnvelopeV3>> {
pub async fn get_thread(account_id: u64, thread_id: u64) -> RustMailerResult<Vec<Envelope>> {
let envelopes = filter_by_secondary_key_impl::<EmailEnvelopeV3>(
DB_MANAGER.envelope_db(),
EmailEnvelopeV3Key::thread_id,
@@ -308,7 +308,7 @@ impl EmailEnvelopeV3 {
}
});
Ok(result)
Ok(result.into_iter().map(Envelope::from).collect())
}
pub async fn get(envelope_id: u64) -> RustMailerResult<Option<EmailEnvelopeV3>> {
+1
View File
@@ -5,3 +5,4 @@
pub mod disk;
pub mod imap;
pub mod vendor;
pub mod model;
+160
View File
@@ -0,0 +1,160 @@
// Copyright © 2025 rustmailer.com
// Licensed under RustMailer License Agreement v1.0
// Unauthorized copying, modification, or distribution is prohibited.
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use crate::{calculate_hash, id, modules::{
cache::imap::{envelope::Received, mailbox::EnvelopeFlag, v2::EmailEnvelopeV3},
common::Addr,
imap::section::{EmailBodyPart, ImapAttachment},
}};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
pub struct Envelope {
/// The unique ID of the message, either IMAP UID or Gmail API MID.
///
/// - For IMAP accounts, this is the UID converted to a string.
/// - For Gmail API accounts, this is the message ID returned by the API.
pub id: String,
/// The ID of the account owning the email.
pub account_id: u64,
/// The unique identifier of the mailbox where the email is stored (e.g., `MailBox::id`).
/// Used for indexing to avoid updating indexes when mailboxes are renamed.
pub mailbox_id: u64,
/// The decoded, human-readable name of the mailbox (e.g., "INBOX", "Sent").
pub mailbox_name: String,
/// The date and time the email was received by the server, as a Unix timestamp in milliseconds.
/// If `None`, the internal date is unavailable.
pub internal_date: Option<i64>,
/// The size of the email in bytes.
pub size: u32,
/// The flags associated with the email (e.g., `\Seen`, `\Answered`, `\Flagged`).
/// Represented as a list of `EnvelopeFlag` for standard or custom flags.
///
/// **Note:** Available only for IMAP accounts.
pub flags: Option<Vec<EnvelopeFlag>>,
/// A hash of the email's flags for efficient comparison or indexing.
///
/// **Note:** Available only for IMAP accounts.
pub flags_hash: Option<u64>,
/// The blind carbon copy (BCC) recipient(s) of the email, if any.
pub bcc: Option<Vec<Addr>>,
/// The carbon copy (CC) recipient(s) of the email, if any.
pub cc: Option<Vec<Addr>>,
/// The date the email was sent, as a Unix timestamp in milliseconds, if available.
pub date: Option<i64>,
/// The sender's address, including name and email, if available.
pub from: Option<Addr>,
/// The message ID of the email to which this email is a reply, if applicable.
pub in_reply_to: Option<String>,
/// The actual sender's address, if different from the `from` field.
pub sender: Option<Addr>,
/// The return address for undeliverable emails, if specified.
pub return_address: Option<String>,
/// The unique message ID of the email, typically used for threading.
pub message_id: Option<String>,
/// The subject of the email, if available.
pub subject: Option<String>,
/// The name of the thread this email belongs to, if applicable.
pub thread_name: Option<String>,
/// The identifier of the thread this email belongs to.
/// This is computed based on `in_reply_to` / `references` / `message_id`.
pub thread_id: u64,
/// The MIME version of the email (e.g., "1.0"), if specified.
pub mime_version: Option<String>,
/// A list of message IDs referenced by this email, used for threading.
pub references: Option<Vec<String>>,
/// The address(es) to which replies should be sent, if specified.
pub reply_to: Option<Vec<Addr>>,
/// The primary recipient(s) of the email, if any.
pub to: Option<Vec<Addr>>,
/// A list of attachments included in the email, if any.
///
/// Each `ImapAttachment` item contains metadata including the part ID and MIME type,
/// which indicates the exact location of the attachment in the raw message structure.
/// This allows the backend to directly fetch specific attachments without retrieving
/// the entire message content.
///
/// This is particularly useful for accounts configured with minimal sync, where full
/// message bodies are not cached locally. By including this data in the API response,
/// the client can request to download only the required attachment via a follow-up
/// API call, improving both efficiency and user experience.
///
/// Developers do not need to understand the internal IMAP part structure — this
/// metadata provides a clean abstraction for fetching specific attachments.
/// **Note:** Available only for IMAP accounts.
pub attachments: Option<Vec<ImapAttachment>>,
/// Metadata for the email's body parts (e.g., plain text, HTML), if available.
///
/// Each `EmailBodyPart` contains detailed metadata (such as part ID, content type,
/// and charset) describing a portion of the email body. This enables precise access
/// to body content, such as plain text or HTML sections, without downloading the full
/// raw message from the server.
///
/// This is especially helpful for lightweight clients or minimized-sync accounts that
/// do not cache full email content. The frontend can pass this metadata back to the
/// server to retrieve only the desired portion of the message (e.g., the HTML body),
/// which significantly reduces bandwidth and latency.
///
/// By abstracting the complexity of MIME part navigation, developers can efficiently
/// retrieve specific parts of an email without handling the low-level IMAP structure.
/// **Note:** Available only for IMAP accounts.
pub body_meta: Option<Vec<EmailBodyPart>>,
/// Details about how the email was received, if available.
/// **Note:** Available only for IMAP accounts.
pub received: Option<Received>,
/// A list of labels applied to the message.
///
/// Each element is a string representing a Gmail label name (e.g., "INBOX", "UNREAD").
/// This field reflects the current labels associated with the email.
///
/// **Note:** This field is populated only for Gmail API accounts. For other account types, it will be empty.
pub labels: Vec<String>,
}
impl Envelope {
pub fn compute_thread_id(&self) -> u64 {
if self.in_reply_to.is_some() && self.references.as_ref().map_or(false, |r| !r.is_empty()) {
return calculate_hash!(&self.references.as_ref().unwrap()[0]);
}
if let Some(message_id) = self.message_id.as_ref() {
return calculate_hash!(message_id);
}
id!(128)
}
}
impl From<EmailEnvelopeV3> for Envelope {
fn from(value: EmailEnvelopeV3) -> Self {
Self {
id: value.uid.to_string(),
account_id: value.account_id,
mailbox_id: value.mailbox_id,
mailbox_name: value.mailbox_name,
internal_date: value.internal_date,
size: value.size,
flags: Some(value.flags),
flags_hash: Some(value.flags_hash),
bcc: value.bcc,
cc: value.cc,
date: value.date,
from: value.from,
in_reply_to: value.in_reply_to,
sender: value.sender,
return_address: value.return_address,
message_id: value.message_id,
subject: value.subject,
thread_name: value.thread_name,
thread_id: value.thread_id,
mime_version: value.mime_version,
references: value.references,
reply_to: value.reply_to,
to: value.to,
attachments: value.attachments,
body_meta: value.body_meta,
received: value.received,
labels: value.labels,
}
}
}
+45 -4
View File
@@ -5,10 +5,13 @@
use crate::{
calculate_hash, id,
modules::{
cache::imap::{
address::AddressEntity,
thread::{EmailThread, EmailThreadKey},
v2::EmailEnvelopeV3,
cache::{
imap::{
address::AddressEntity,
thread::{EmailThread, EmailThreadKey},
v2::EmailEnvelopeV3,
},
model::Envelope,
},
common::Addr,
database::{
@@ -386,4 +389,42 @@ impl GmailEnvelope {
labels,
}
}
pub fn into_envelope(self, label_map: &AHashMap<String, String>) -> Envelope {
let labels: Vec<String> = self
.label_ids
.into_iter()
.filter_map(|id| label_map.get(&id).cloned())
.collect();
Envelope {
id: self.id,
account_id: self.account_id,
mailbox_id: self.label_id,
mailbox_name: self.label_name,
internal_date: Some(self.internal_date),
size: self.size,
flags: None,
flags_hash: None,
bcc: self.bcc,
cc: self.cc,
date: self.date,
from: self.from,
in_reply_to: self.in_reply_to,
sender: self.sender,
return_address: None,
message_id: self.message_id,
subject: self.subject,
thread_name: None,
thread_id: self.thread_id,
mime_version: self.mime_version,
references: self.references,
reply_to: self.reply_to,
to: self.to,
attachments: None,
body_meta: None,
received: None,
labels,
}
}
}
+6 -4
View File
@@ -311,7 +311,7 @@ async fn dispatch_new_email_notification(
let full_message =
GmailClient::get_full_messages(account.id, account.use_proxy, &message.id).await?;
let message_content: FullMessageContent = full_message.try_into()?;
let mut envelope = message.into_v3(&label_map);
let mut envelope = message.into_envelope(&label_map);
envelope.thread_id = envelope.compute_thread_id();
EVENT_CHANNEL
.queue(Event::new(
@@ -323,14 +323,17 @@ async fn dispatch_new_email_notification(
account_id: account.id,
account_email: account.email.clone(),
mailbox_name: envelope.mailbox_name.clone(),
uid: envelope.uid,
id: envelope.id,
internal_date: envelope.internal_date,
date: envelope.date,
from: envelope.from,
subject: envelope.subject,
to: envelope.to,
size: envelope.size,
flags: envelope.flags.into_iter().map(|f| f.to_string()).collect(),
flags: envelope
.flags
.map(|f| f.into_iter().map(|f| f.to_string()).collect())
.unwrap_or_default(),
cc: envelope.cc,
bcc: envelope.bcc,
in_reply_to: envelope.in_reply_to,
@@ -344,7 +347,6 @@ async fn dispatch_new_email_notification(
.as_ref()
.map(|atts| atts.iter().cloned().map(Attachment::from).collect()),
thread_id: envelope.thread_id,
mid: envelope.mid,
labels: envelope.labels,
}),
),