mirror of
https://github.com/rustmailer/rustmailer.git
synced 2026-08-25 00:00:43 +00:00
refactor: merge uid and mid into a single id field in EmailAddedToFolder and EmailEnvelopeV3
This commit is contained in:
+9
-13
@@ -715,8 +715,10 @@ message EmailEnvelope {
|
||||
uint64 mailbox_id = 2;
|
||||
// The name of the mailbox this email is in.
|
||||
string mailbox_name = 3;
|
||||
// The unique identifier of the message within its mailbox.
|
||||
uint32 uid = 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.
|
||||
// - For Gmail API accounts, this is the message ID returned by the API.
|
||||
string id = 4;
|
||||
// Optional: The internal date and time of the message on the server (Unix timestamp).
|
||||
optional int64 internal_date = 5;
|
||||
// The size of the message in bytes.
|
||||
@@ -724,7 +726,7 @@ message EmailEnvelope {
|
||||
// A list of flags currently set on the message (e.g., \Seen, \Answered).
|
||||
repeated EnvelopeFlag flags = 7;
|
||||
// A hash of the message flags for quick comparison.
|
||||
uint64 flags_hash = 8;
|
||||
optional uint64 flags_hash = 8;
|
||||
// A list of blind carbon copy (BCC) recipients.
|
||||
repeated Addr bcc = 9;
|
||||
// A list of carbon copy (CC) recipients.
|
||||
@@ -762,17 +764,11 @@ message EmailEnvelope {
|
||||
// The identifier of the thread this email belongs to.
|
||||
// This is computed based on `in_reply_to` / `references` / `message_id`.
|
||||
uint64 thread_id = 26;
|
||||
// The `mid` field is reserved for potential integration with other backend models.
|
||||
// For instance, it can be used to store the email index or ID from external services like the Gmail API.
|
||||
// This ID could be used for reference or identification purposes in scenarios where an external service
|
||||
// provides an identifier for the email in question.
|
||||
// This field is optional, meaning that it may be `None` if no external service identifier is available.
|
||||
optional string mid = 27;
|
||||
// A list of labels applied to the message.
|
||||
// Each element is a string representing a Gmail label ID (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.
|
||||
repeated string label_ids = 28;
|
||||
// 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.
|
||||
repeated string labels = 27;
|
||||
}
|
||||
|
||||
// FetchMessageContentRequest is used to fetch specific content sections of an email message.
|
||||
|
||||
Vendored
+1
-2
@@ -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![],
|
||||
}),
|
||||
),
|
||||
|
||||
Vendored
+8
-4
@@ -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,
|
||||
|
||||
Vendored
+12
-12
@@ -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>> {
|
||||
|
||||
Vendored
+1
@@ -5,3 +5,4 @@
|
||||
pub mod disk;
|
||||
pub mod imap;
|
||||
pub mod vendor;
|
||||
pub mod model;
|
||||
Vendored
+160
@@ -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
@@ -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
@@ -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,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -13,6 +13,8 @@ use crate::modules::common::log::Tracing;
|
||||
use crate::modules::common::timeout::Timeout;
|
||||
use crate::modules::common::tls::rustls_config;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::grpc::service::hook::RustMailerEventHooksService;
|
||||
use crate::modules::grpc::service::rustmailer_grpc::EventHooksServiceServer;
|
||||
use crate::modules::settings::cli::CompressionAlgorithm;
|
||||
use crate::modules::{
|
||||
error::RustMailerResult,
|
||||
@@ -72,12 +74,16 @@ pub async fn start_grpc_server() -> RustMailerResult<()> {
|
||||
.add_file_descriptor_set(FILE_DESCRIPTOR_SET)
|
||||
.build(),
|
||||
);
|
||||
|
||||
route = add_service!(
|
||||
route,
|
||||
AccountServiceServer<RustMailerAccountService>,
|
||||
RustMailerAccountService
|
||||
);
|
||||
route = add_service!(
|
||||
route,
|
||||
EventHooksServiceServer<RustMailerEventHooksService>,
|
||||
RustMailerEventHooksService
|
||||
);
|
||||
route = add_service!(
|
||||
route,
|
||||
AutoConfigServiceServer<RustMailerAutoConfigService>,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::modules::{
|
||||
cache::imap::{
|
||||
envelope::Received,
|
||||
mailbox::{EmailFlag, EnvelopeFlag},
|
||||
v2::EmailEnvelopeV3,
|
||||
cache::{
|
||||
imap::{
|
||||
envelope::Received,
|
||||
mailbox::{EmailFlag, EnvelopeFlag},
|
||||
},
|
||||
model::Envelope,
|
||||
},
|
||||
common::Addr,
|
||||
grpc::service::rustmailer_grpc::{self},
|
||||
@@ -132,8 +134,8 @@ impl TryFrom<rustmailer_grpc::FlagMessageRequest> for FlagMessageRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CursorDataPage<EmailEnvelopeV3>> for rustmailer_grpc::CursorDataPage {
|
||||
fn from(value: CursorDataPage<EmailEnvelopeV3>) -> Self {
|
||||
impl From<CursorDataPage<Envelope>> for rustmailer_grpc::CursorDataPage {
|
||||
fn from(value: CursorDataPage<Envelope>) -> Self {
|
||||
Self {
|
||||
next_page_token: value.next_page_token,
|
||||
page_size: value.page_size,
|
||||
@@ -144,8 +146,8 @@ impl From<CursorDataPage<EmailEnvelopeV3>> for rustmailer_grpc::CursorDataPage {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DataPage<EmailEnvelopeV3>> for rustmailer_grpc::PagedMessages {
|
||||
fn from(value: DataPage<EmailEnvelopeV3>) -> Self {
|
||||
impl From<DataPage<Envelope>> for rustmailer_grpc::PagedMessages {
|
||||
fn from(value: DataPage<Envelope>) -> Self {
|
||||
Self {
|
||||
current_page: value.current_page,
|
||||
page_size: value.page_size,
|
||||
@@ -156,16 +158,19 @@ impl From<DataPage<EmailEnvelopeV3>> for rustmailer_grpc::PagedMessages {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailEnvelopeV3> for rustmailer_grpc::EmailEnvelope {
|
||||
fn from(value: EmailEnvelopeV3) -> Self {
|
||||
impl From<Envelope> for rustmailer_grpc::EmailEnvelope {
|
||||
fn from(value: Envelope) -> Self {
|
||||
Self {
|
||||
account_id: value.account_id,
|
||||
mailbox_id: value.mailbox_id,
|
||||
mailbox_name: value.mailbox_name,
|
||||
uid: value.uid,
|
||||
id: value.id,
|
||||
internal_date: value.internal_date,
|
||||
size: value.size,
|
||||
flags: value.flags.into_iter().map(Into::into).collect(),
|
||||
flags: value
|
||||
.flags
|
||||
.map(|f| f.into_iter().map(Into::into).collect())
|
||||
.unwrap_or_default(),
|
||||
flags_hash: value.flags_hash,
|
||||
bcc: value
|
||||
.bcc
|
||||
@@ -215,8 +220,7 @@ impl From<EmailEnvelopeV3> for rustmailer_grpc::EmailEnvelope {
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
received: value.received.map(Into::into),
|
||||
mid: value.mid,
|
||||
label_ids: value.labels,
|
||||
labels: value.labels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ impl RustMailerEvent {
|
||||
account_id: id!(64),
|
||||
account_email: account_email.clone(),
|
||||
mailbox_name: "INBOX".into(),
|
||||
uid: 1001,
|
||||
id: "1001".to_string(),
|
||||
internal_date: Some(timestamp),
|
||||
date: Some(timestamp),
|
||||
size: 2048,
|
||||
@@ -195,7 +195,6 @@ impl RustMailerEvent {
|
||||
size: 1024,
|
||||
file_type: "application/pdf".into(),
|
||||
}]),
|
||||
mid: None,
|
||||
labels: vec![]
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,8 +18,10 @@ pub struct EmailAddedToFolder {
|
||||
pub account_email: String,
|
||||
/// Name of the mailbox (folder) where the email was added.
|
||||
pub mailbox_name: String,
|
||||
/// Unique identifier (UID) of the email within the mailbox.
|
||||
pub uid: u32,
|
||||
/// 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,
|
||||
/// Optional internal date (in milliseconds) assigned to the email by the server.
|
||||
pub internal_date: Option<i64>,
|
||||
/// Optional date (in milliseconds) of the email, typically from the email's header.
|
||||
@@ -55,13 +57,6 @@ pub struct EmailAddedToFolder {
|
||||
pub to: Option<Vec<Addr>>,
|
||||
/// Optional list of attachments included in the email.
|
||||
pub attachments: Option<Vec<Attachment>>,
|
||||
/// The `mid` field is reserved for potential integration with other backend models.
|
||||
/// For instance, it can be used to store the email index or ID from external services like the Gmail API.
|
||||
/// This ID could be used for reference or identification purposes in scenarios where an external service
|
||||
/// provides an identifier for the email in question.
|
||||
///
|
||||
/// This field is optional, meaning that it may be `None` if no external service identifier is available.
|
||||
pub mid: Option<String>,
|
||||
/// A list of labels applied to the message.
|
||||
///
|
||||
/// Each element is a string representing a Gmail label ID (e.g., "INBOX", "UNREAD").
|
||||
|
||||
+15
-15
@@ -2,36 +2,36 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use std::{pin::Pin, time::Duration};
|
||||
use std::pin::Pin;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, BufWriter};
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
pub trait SessionStream: AsyncRead + AsyncWrite + Unpin + Send + Sync + std::fmt::Debug {
|
||||
/// Change the read timeout on the session stream.
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>);
|
||||
// Change the read timeout on the session stream.
|
||||
// fn set_read_timeout(&mut self, timeout: Option<Duration>);
|
||||
}
|
||||
|
||||
impl SessionStream for Box<dyn SessionStream> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout(timeout);
|
||||
}
|
||||
// fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
// self.as_mut().set_read_timeout(timeout);
|
||||
// }
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionStream for tokio_rustls::client::TlsStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().0.set_read_timeout(timeout);
|
||||
}
|
||||
// fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
// self.get_mut().0.set_read_timeout(timeout);
|
||||
// }
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionStream for BufWriter<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
// fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
// self.get_mut().set_read_timeout(timeout);
|
||||
// }
|
||||
}
|
||||
impl<T: AsyncRead + AsyncWrite + Send + Sync + std::fmt::Debug> SessionStream
|
||||
for Pin<Box<TimeoutStream<T>>>
|
||||
{
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout_pinned(timeout);
|
||||
}
|
||||
// fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
// self.as_mut().set_read_timeout_pinned(timeout);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
// use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
@@ -64,9 +64,9 @@ impl<T: AsyncWrite + Unpin> AsyncWrite for StatsWrapper<T> {
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionStream for StatsWrapper<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.inner.set_read_timeout(timeout);
|
||||
}
|
||||
// fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
// self.inner.set_read_timeout(timeout);
|
||||
// }
|
||||
}
|
||||
|
||||
impl<T: SessionStream> std::fmt::Debug for StatsWrapper<T> {
|
||||
|
||||
+17
-13
@@ -8,6 +8,7 @@ use crate::{
|
||||
account::{entity::MailerType, v2::AccountV2},
|
||||
cache::{
|
||||
imap::{mailbox::MailBox, thread::EmailThread, v2::EmailEnvelopeV3},
|
||||
model::Envelope,
|
||||
vendor::gmail::sync::{
|
||||
client::GmailClient, envelope::GmailEnvelope, labels::GmailLabels,
|
||||
},
|
||||
@@ -29,7 +30,7 @@ pub async fn list_messages_in_mailbox(
|
||||
page_size: u64,
|
||||
remote: bool,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<Envelope>> {
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
if page_size == 0 {
|
||||
return Err(raise_error!(
|
||||
@@ -73,7 +74,7 @@ async fn fetch_remote_messages(
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<Envelope>> {
|
||||
match account.mailer_type {
|
||||
MailerType::ImapSmtp => {
|
||||
let page = decode_page_token(next_page_token)?;
|
||||
@@ -167,15 +168,15 @@ async fn fetch_remote_messages(
|
||||
})
|
||||
.await?;
|
||||
|
||||
let envelopes: Vec<EmailEnvelopeV3> = batch_messages
|
||||
let envelopes: Vec<Envelope> = batch_messages
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let mut envelope: GmailEnvelope = m.try_into()?;
|
||||
envelope.account_id = account_id;
|
||||
envelope.label_name = mailbox_name.into();
|
||||
Ok(envelope.into_v3(&label_map))
|
||||
Ok(envelope.into_envelope(&label_map))
|
||||
})
|
||||
.collect::<RustMailerResult<Vec<EmailEnvelopeV3>>>()?;
|
||||
.collect::<RustMailerResult<Vec<Envelope>>>()?;
|
||||
|
||||
let total_pages = (total as f64 / page_size as f64).ceil() as u64;
|
||||
|
||||
@@ -194,11 +195,11 @@ async fn process_fetches(
|
||||
fetches: Vec<Fetch>,
|
||||
account_id: u64,
|
||||
mailbox_name: &str,
|
||||
) -> RustMailerResult<Vec<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<Vec<Envelope>> {
|
||||
let mut envelopes = Vec::with_capacity(fetches.len());
|
||||
for fetch in fetches {
|
||||
let envelope = extract_envelope(&fetch, account_id, mailbox_name)?;
|
||||
envelopes.push(envelope);
|
||||
envelopes.push(envelope.into());
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
||||
@@ -209,7 +210,7 @@ async fn fetch_local_messages(
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<Envelope>> {
|
||||
let page = decode_page_token(next_page_token)?;
|
||||
match account.mailer_type {
|
||||
MailerType::ImapSmtp => {
|
||||
@@ -251,7 +252,7 @@ async fn fetch_local_messages(
|
||||
page_size,
|
||||
total_items,
|
||||
Some(total_pages),
|
||||
items,
|
||||
items.into_iter().map(Envelope::from).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -289,7 +290,7 @@ async fn fetch_local_messages(
|
||||
page_size,
|
||||
total_items,
|
||||
Some(total_pages),
|
||||
items.into_iter().map(|e| e.into_v3(&map)).collect(),
|
||||
items.into_iter().map(|e| e.into_envelope(&map)).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -302,7 +303,7 @@ pub async fn list_threads_in_mailbox(
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<DataPage<Envelope>> {
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
validate_pagination_params(page, page_size)?;
|
||||
if account.minimal_sync() {
|
||||
@@ -346,7 +347,7 @@ pub async fn list_threads_in_mailbox(
|
||||
pub async fn get_thread_messages(
|
||||
account_id: u64,
|
||||
thread_id: u64,
|
||||
) -> RustMailerResult<Vec<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<Vec<Envelope>> {
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
if account.minimal_sync() {
|
||||
return Err(raise_error!(
|
||||
@@ -365,7 +366,10 @@ pub async fn get_thread_messages(
|
||||
MailerType::GmailApi => {
|
||||
let envelopes = GmailEnvelope::get_thread(account_id, thread_id).await?;
|
||||
let map = GmailClient::label_map(account_id, account.use_proxy).await?;
|
||||
Ok(envelopes.into_iter().map(|e| e.into_v3(&map)).collect())
|
||||
Ok(envelopes
|
||||
.into_iter()
|
||||
.map(|e| e.into_envelope(&map))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::modules::account::entity::MailerType;
|
||||
use crate::modules::cache::imap::address::AddressEntity;
|
||||
use crate::modules::cache::imap::sync::flow::generate_uid_sequence_hashset;
|
||||
use crate::modules::cache::imap::v2::EmailEnvelopeV3;
|
||||
use crate::modules::cache::model::Envelope;
|
||||
use crate::modules::cache::vendor::gmail::sync::client::GmailClient;
|
||||
use crate::modules::cache::vendor::gmail::sync::envelope::GmailEnvelope;
|
||||
use crate::modules::common::decode_page_token;
|
||||
@@ -485,7 +486,7 @@ impl MessageSearchRequest {
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<Envelope>> {
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
match account.mailer_type {
|
||||
MailerType::ImapSmtp => {
|
||||
@@ -504,7 +505,7 @@ impl MessageSearchRequest {
|
||||
account: &AccountV2,
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<Envelope>> {
|
||||
if page_size == 0 {
|
||||
return Err(raise_error!(
|
||||
"page_size must be greater than 0.".into(),
|
||||
@@ -571,14 +572,14 @@ impl MessageSearchRequest {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let envelopes: Vec<EmailEnvelopeV3> = batch_messages
|
||||
let envelopes: Vec<Envelope> = batch_messages
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let mut envelope: GmailEnvelope = m.try_into()?;
|
||||
envelope.account_id = account_id;
|
||||
Ok(envelope.into_v3(&label_map))
|
||||
Ok(envelope.into_envelope(&label_map))
|
||||
})
|
||||
.collect::<RustMailerResult<Vec<EmailEnvelopeV3>>>()?;
|
||||
.collect::<RustMailerResult<Vec<Envelope>>>()?;
|
||||
|
||||
let total_pages = (total as f64 / page_size as f64).ceil() as u64;
|
||||
|
||||
@@ -597,7 +598,7 @@ impl MessageSearchRequest {
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<Envelope>> {
|
||||
// Validate page and page_size
|
||||
if page_size == 0 {
|
||||
return Err(raise_error!(
|
||||
@@ -657,7 +658,7 @@ impl MessageSearchRequest {
|
||||
let mut envelopes = Vec::new();
|
||||
for fetch in fetches {
|
||||
let envelope = extract_envelope(&fetch, account.id, mailbox)?;
|
||||
envelopes.push(envelope);
|
||||
envelopes.push(envelope.into());
|
||||
}
|
||||
|
||||
let next_page_token = if page == total_pages {
|
||||
@@ -717,7 +718,7 @@ impl MessageSearchRequest {
|
||||
let mut envelopes = Vec::new();
|
||||
for fetch in fetches {
|
||||
let envelope = extract_envelope(&fetch, account.id, mailbox)?;
|
||||
envelopes.push(envelope);
|
||||
envelopes.push(envelope.into());
|
||||
}
|
||||
let next_page_token = if page == total_pages {
|
||||
None
|
||||
@@ -754,7 +755,7 @@ impl UnifiedSearchRequest {
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<DataPage<Envelope>> {
|
||||
if page == 0 || page_size == 0 {
|
||||
return Err(raise_error!(
|
||||
"'page' and 'page_size' must be greater than 0.".into(),
|
||||
@@ -803,12 +804,17 @@ impl UnifiedSearchRequest {
|
||||
for (id, account_id, _) in result.items {
|
||||
let account = AccountV2::get(account_id).await?;
|
||||
let envelope = match account.mailer_type {
|
||||
MailerType::ImapSmtp => EmailEnvelopeV3::get(id).await?.ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!("Failed to get EmailEnvelope for hash {id} in search operation"),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?,
|
||||
MailerType::ImapSmtp => EmailEnvelopeV3::get(id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!(
|
||||
"Failed to get EmailEnvelope for hash {id} in search operation"
|
||||
),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?
|
||||
.into(),
|
||||
MailerType::GmailApi => {
|
||||
let label_map = GmailClient::label_map(account_id, account.use_proxy).await?;
|
||||
let envelope = GmailEnvelope::get(id).await?.ok_or_else(|| {
|
||||
@@ -819,7 +825,7 @@ impl UnifiedSearchRequest {
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
envelope.into_v3(&label_map)
|
||||
envelope.into_envelope(&label_map)
|
||||
}
|
||||
};
|
||||
items.push(envelope);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::current_datetime;
|
||||
use crate::modules::cache::imap::v2::EmailEnvelopeV3;
|
||||
use crate::modules::cache::model::Envelope;
|
||||
use crate::modules::common::auth::ClientContext;
|
||||
use crate::modules::message::append::AppendReplyToDraftRequest;
|
||||
use crate::modules::message::attachment::{retrieve_email_attachment, AttachmentRequest};
|
||||
@@ -135,7 +135,7 @@ impl MessageApi {
|
||||
/// lists messages in descending order; otherwise, ascending. internal date
|
||||
desc: Query<Option<bool>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<CursorDataPage<EmailEnvelopeV3>>> {
|
||||
) -> ApiResult<Json<CursorDataPage<Envelope>>> {
|
||||
let remote = remote.0.unwrap_or(false);
|
||||
let desc = desc.0.unwrap_or(false);
|
||||
let account_id = account_id.0;
|
||||
@@ -175,7 +175,7 @@ impl MessageApi {
|
||||
/// lists messages in descending order; otherwise, ascending. internal date
|
||||
desc: Query<Option<bool>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<EmailEnvelopeV3>>> {
|
||||
) -> ApiResult<Json<DataPage<Envelope>>> {
|
||||
let desc = desc.0.unwrap_or(false);
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
@@ -199,7 +199,7 @@ impl MessageApi {
|
||||
// Thread ID
|
||||
thread_id: Query<u64>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<Vec<EmailEnvelopeV3>>> {
|
||||
) -> ApiResult<Json<Vec<Envelope>>> {
|
||||
let account_id = account_id.0;
|
||||
context.require_account_access(account_id)?;
|
||||
|
||||
@@ -319,7 +319,7 @@ impl MessageApi {
|
||||
/// specifying the search criteria (e.g., keywords, flags).
|
||||
payload: Json<MessageSearchRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<CursorDataPage<EmailEnvelopeV3>>> {
|
||||
) -> ApiResult<Json<CursorDataPage<Envelope>>> {
|
||||
let request = payload.0;
|
||||
let desc = desc.0.unwrap_or(false);
|
||||
let account_id = account_id.0;
|
||||
@@ -351,7 +351,7 @@ impl MessageApi {
|
||||
payload: Json<UnifiedSearchRequest>,
|
||||
/// Request context (includes authentication and permissions).
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<EmailEnvelopeV3>>> {
|
||||
) -> ApiResult<Json<DataPage<Envelope>>> {
|
||||
let mut request = payload.0;
|
||||
let desc = desc.0.unwrap_or(false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user