mirror of
https://github.com/rustmailer/rustmailer.git
synced 2026-08-19 00:01:07 +00:00
refactor(list-messages): switch from page-based to cursor-based pagination
- Changed `list-messages` API from page/page_size to cursor/next_page_token - Aligns pagination approach with Gmail API for easier integration - Frontend updates to handle cursor-based navigation
This commit is contained in:
@@ -993,9 +993,11 @@ message ListMessagesRequest {
|
||||
uint64 account_id = 1;
|
||||
// The name of the mailbox to list messages from.
|
||||
string mailbox_name = 2;
|
||||
// The requested page number (1-based).
|
||||
uint64 page = 3;
|
||||
// The number of messages to return per page.
|
||||
// The token for fetching the next page of results in pagination.
|
||||
// - If `None`, this indicates that the first page should be returned.
|
||||
// - If `Some(token)`, the page corresponding to this token will be fetched.
|
||||
optional string next_page_token = 3;
|
||||
// The number of messages to return per page. max 500
|
||||
uint64 page_size = 4;
|
||||
// If true, fetches messages directly from the remote server.
|
||||
bool remote = 5;
|
||||
@@ -1152,7 +1154,7 @@ service MessageService {
|
||||
// Updates flags on messages within a mailbox.
|
||||
rpc UpdateMessageFlags(FlagMessageRequest) returns (Empty);
|
||||
// Lists messages within a mailbox with pagination.
|
||||
rpc ListMessages(ListMessagesRequest) returns (PagedMessages);
|
||||
rpc ListMessages(ListMessagesRequest) returns (CursorDataPage);
|
||||
// Lists threads within a mailbox with pagination.
|
||||
rpc ListThreads(ListThreadsRequest) returns (PagedMessages);
|
||||
// Get thread's envelopes within a mailbox.
|
||||
|
||||
+2
-2
@@ -202,9 +202,9 @@ impl GmailClient {
|
||||
account_id: u64,
|
||||
use_proxy: Option<u64>,
|
||||
label_id: &str,
|
||||
page_token: Option<String>,
|
||||
page_token: Option<&str>,
|
||||
after: Option<&str>,
|
||||
max_results: u32,
|
||||
max_results: u64,
|
||||
) -> RustMailerResult<MessageList> {
|
||||
let mut url = format!(
|
||||
"https://gmail.googleapis.com/gmail/v1/users/me/messages?labelIds={}&maxResults={}",
|
||||
|
||||
+4
-4
@@ -43,9 +43,9 @@ pub async fn fetch_and_save_since_date(
|
||||
account_id,
|
||||
use_proxy,
|
||||
&label.label_id,
|
||||
page_token,
|
||||
page_token.as_deref(),
|
||||
Some(date),
|
||||
ENVELOPE_BATCH_SIZE,
|
||||
ENVELOPE_BATCH_SIZE as u64,
|
||||
)
|
||||
.await?;
|
||||
// The total number of messages can only be retrieved via an API query
|
||||
@@ -156,9 +156,9 @@ pub async fn fetch_and_save_full_label(
|
||||
account_id,
|
||||
use_proxy,
|
||||
&label.label_id,
|
||||
page_token,
|
||||
page_token.as_deref(),
|
||||
None,
|
||||
ENVELOPE_BATCH_SIZE,
|
||||
ENVELOPE_BATCH_SIZE as u64,
|
||||
)
|
||||
.await?;
|
||||
// Update page_token returned by Gmail API
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::base64_encode_url_safe;
|
||||
use crate::base64_decode_url_safe;
|
||||
use crate::modules::error::RustMailerResult;
|
||||
use crate::raise_error;
|
||||
|
||||
use super::error::code::ErrorCode;
|
||||
use super::error::RustMailerError;
|
||||
@@ -22,14 +24,14 @@ use tracing::error;
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
pub mod log;
|
||||
pub mod lru;
|
||||
pub mod paginated;
|
||||
pub mod parallel;
|
||||
pub mod rustls;
|
||||
pub mod signal;
|
||||
pub mod timeout;
|
||||
pub mod tls;
|
||||
pub mod validator;
|
||||
pub mod lru;
|
||||
pub mod parallel;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Object)]
|
||||
pub struct Addr {
|
||||
@@ -219,3 +221,22 @@ impl ResponseError for RustMailerError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_page_token(next_page_token: Option<&str>) -> RustMailerResult<u64> {
|
||||
match next_page_token {
|
||||
Some(token) => {
|
||||
let decoded = base64_decode_url_safe!(token)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
.and_then(|s| s.parse::<u64>().ok());
|
||||
|
||||
decoded.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"Invalid next_page_token: not a valid page token".into(),
|
||||
ErrorCode::InvalidParameter
|
||||
)
|
||||
})
|
||||
}
|
||||
None => Ok(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,13 +85,13 @@ impl MessageService for RustMailerMessageService {
|
||||
async fn list_messages(
|
||||
&self,
|
||||
request: Request<ListMessagesRequest>,
|
||||
) -> Result<Response<PagedMessages>, Status> {
|
||||
) -> Result<Response<CursorDataPage>, Status> {
|
||||
let req = require_account_access(request, |r| r.account_id)?;
|
||||
|
||||
let result = list_messages_in_mailbox(
|
||||
req.account_id,
|
||||
&req.mailbox_name,
|
||||
req.page,
|
||||
req.next_page_token.as_deref(),
|
||||
req.page_size,
|
||||
req.remote,
|
||||
req.desc,
|
||||
|
||||
@@ -37,7 +37,7 @@ async fn test1() {
|
||||
let request = ListMessagesRequest {
|
||||
account_id: id!(64),
|
||||
mailbox_name: "INBOX".into(),
|
||||
page: 1,
|
||||
next_page_token: None,
|
||||
page_size: 10,
|
||||
remote: false,
|
||||
desc: true,
|
||||
|
||||
+204
-53
@@ -3,7 +3,7 @@
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::{
|
||||
encode_mailbox_name,
|
||||
base64_encode_url_safe, encode_mailbox_name,
|
||||
modules::{
|
||||
account::{entity::MailerType, v2::AccountV2},
|
||||
cache::{
|
||||
@@ -12,10 +12,11 @@ use crate::{
|
||||
client::GmailClient, envelope::GmailEnvelope, labels::GmailLabels,
|
||||
},
|
||||
},
|
||||
common::{decode_page_token, parallel::run_with_limit},
|
||||
context::executors::RUST_MAIL_CONTEXT,
|
||||
envelope::extractor::extract_envelope,
|
||||
error::{code::ErrorCode, RustMailerResult},
|
||||
rest::response::DataPage,
|
||||
rest::response::{CursorDataPage, DataPage},
|
||||
},
|
||||
raise_error,
|
||||
};
|
||||
@@ -24,19 +25,29 @@ use async_imap::types::Fetch;
|
||||
pub async fn list_messages_in_mailbox(
|
||||
account_id: u64,
|
||||
mailbox_name: &str,
|
||||
page: u64,
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
remote: bool,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
let account = AccountV2::check_account_active(account_id, false).await?;
|
||||
validate_pagination_params(page, page_size)?;
|
||||
if page_size == 0 {
|
||||
return Err(raise_error!(
|
||||
"page_size must be greater than 0.".into(),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
if page_size > 500 {
|
||||
return Err(raise_error!(
|
||||
"The page_size exceeds the maximum allowed limit of 500.".into(),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
let remote = remote || account.minimal_sync();
|
||||
|
||||
if remote {
|
||||
fetch_remote_messages(account_id, mailbox_name, page, page_size, desc).await
|
||||
fetch_remote_messages(&account, mailbox_name, next_page_token, page_size, desc).await
|
||||
} else {
|
||||
fetch_local_messages(&account, mailbox_name, page, page_size, desc).await
|
||||
fetch_local_messages(&account, mailbox_name, next_page_token, page_size, desc).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +58,9 @@ fn validate_pagination_params(page: u64, page_size: u64) -> RustMailerResult<()>
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
if page_size > 1000 {
|
||||
if page_size > 500 {
|
||||
return Err(raise_error!(
|
||||
"The page_size exceeds the maximum allowed limit of 1000.".into(),
|
||||
"The page_size exceeds the maximum allowed limit of 500.".into(),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
@@ -57,37 +68,126 @@ fn validate_pagination_params(page: u64, page_size: u64) -> RustMailerResult<()>
|
||||
}
|
||||
|
||||
async fn fetch_remote_messages(
|
||||
account_id: u64,
|
||||
account: &AccountV2,
|
||||
mailbox_name: &str,
|
||||
page: u64,
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
let excutor = RUST_MAIL_CONTEXT.imap(account_id).await?;
|
||||
let (mut fetches, total_items) = excutor
|
||||
.retrieve_metadata_paginated(
|
||||
page,
|
||||
page_size,
|
||||
encode_mailbox_name!(mailbox_name).as_str(),
|
||||
desc,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
match account.mailer_type {
|
||||
MailerType::ImapSmtp => {
|
||||
let page = decode_page_token(next_page_token)?;
|
||||
let excutor = RUST_MAIL_CONTEXT.imap(account.id).await?;
|
||||
let (mut fetches, total_items) = excutor
|
||||
.retrieve_metadata_paginated(
|
||||
page,
|
||||
page_size,
|
||||
encode_mailbox_name!(mailbox_name).as_str(),
|
||||
desc,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
if total_items == 0 {
|
||||
return Ok(CursorDataPage::new(
|
||||
None,
|
||||
Some(page_size),
|
||||
0,
|
||||
Some(0),
|
||||
vec![],
|
||||
));
|
||||
}
|
||||
|
||||
if desc {
|
||||
fetches.reverse();
|
||||
if desc {
|
||||
fetches.reverse();
|
||||
}
|
||||
|
||||
let total_pages = (total_items as f64 / page_size as f64).ceil() as u64;
|
||||
let envelopes = process_fetches(fetches, account.id, mailbox_name).await?;
|
||||
|
||||
let next_page_token = if page == total_pages {
|
||||
None
|
||||
} else {
|
||||
Some(base64_encode_url_safe!((page + 1).to_string()))
|
||||
};
|
||||
|
||||
Ok(CursorDataPage::new(
|
||||
next_page_token,
|
||||
Some(page_size),
|
||||
total_items,
|
||||
Some(total_pages),
|
||||
envelopes,
|
||||
))
|
||||
}
|
||||
MailerType::GmailApi => {
|
||||
let label_map =
|
||||
GmailClient::reverse_label_map(account.id, account.use_proxy, true).await?;
|
||||
let label_id = label_map.get(mailbox_name).ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!("Label not found for mailbox: {}", mailbox_name),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})?;
|
||||
let message_list = GmailClient::list_messages(
|
||||
account.id,
|
||||
account.use_proxy,
|
||||
label_id,
|
||||
next_page_token,
|
||||
None,
|
||||
page_size,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let total = message_list.result_size_estimate.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"Missing 'resultSizeEstimate' in Gmail API response".into(),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
|
||||
let messages = message_list.messages;
|
||||
let messages = match messages {
|
||||
Some(ref msgs) if !msgs.is_empty() => msgs,
|
||||
_ => {
|
||||
return Ok(CursorDataPage {
|
||||
next_page_token: None,
|
||||
page_size: Some(page_size),
|
||||
total_items: 0,
|
||||
items: vec![],
|
||||
total_pages: Some(0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let account_id = account.id;
|
||||
let use_proxy = account.use_proxy;
|
||||
let next_page_token = message_list.next_page_token;
|
||||
let batch_messages =
|
||||
run_with_limit(5, messages.iter().cloned(), move |index| async move {
|
||||
GmailClient::get_message(account_id, use_proxy, &index.id).await
|
||||
})
|
||||
.await?;
|
||||
|
||||
let envelopes: Vec<EmailEnvelopeV3> = 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))
|
||||
})
|
||||
.collect::<RustMailerResult<Vec<EmailEnvelopeV3>>>()?;
|
||||
|
||||
let total_pages = (total as f64 / page_size as f64).ceil() as u64;
|
||||
|
||||
Ok(CursorDataPage {
|
||||
next_page_token,
|
||||
page_size: Some(page_size),
|
||||
total_items: total,
|
||||
items: envelopes,
|
||||
total_pages: Some(total_pages),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let total_pages = (total_items as f64 / page_size as f64).ceil() as u64;
|
||||
let envelopes = process_fetches(fetches, account_id, mailbox_name).await?;
|
||||
|
||||
Ok(DataPage::new(
|
||||
Some(page),
|
||||
Some(page_size),
|
||||
total_items,
|
||||
Some(total_pages),
|
||||
envelopes,
|
||||
))
|
||||
}
|
||||
|
||||
async fn process_fetches(
|
||||
@@ -106,10 +206,11 @@ async fn process_fetches(
|
||||
async fn fetch_local_messages(
|
||||
account: &AccountV2,
|
||||
mailbox_name: &str,
|
||||
page: u64,
|
||||
next_page_token: Option<&str>,
|
||||
page_size: u64,
|
||||
desc: bool,
|
||||
) -> RustMailerResult<DataPage<EmailEnvelopeV3>> {
|
||||
) -> RustMailerResult<CursorDataPage<EmailEnvelopeV3>> {
|
||||
let page = decode_page_token(next_page_token)?;
|
||||
match account.mailer_type {
|
||||
MailerType::ImapSmtp => {
|
||||
let mailbox = MailBox::get(account.id, mailbox_name).await.map_err(|_| {
|
||||
@@ -120,27 +221,77 @@ async fn fetch_local_messages(
|
||||
ErrorCode::MailBoxNotCached
|
||||
)
|
||||
})?;
|
||||
let DataPage {
|
||||
current_page: _,
|
||||
page_size,
|
||||
total_items,
|
||||
items,
|
||||
total_pages,
|
||||
} = EmailEnvelopeV3::list_messages_in_mailbox(mailbox.id, page, page_size, desc)
|
||||
.await?;
|
||||
|
||||
EmailEnvelopeV3::list_messages_in_mailbox(mailbox.id, page, page_size, desc).await
|
||||
if total_items == 0 {
|
||||
Ok(CursorDataPage::new(None, page_size, 0, None, vec![]))
|
||||
} else {
|
||||
let total_pages = total_pages.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"Internal error: total_pages is None (this should never happen)".into(),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
|
||||
let next_page_token = if page == total_pages {
|
||||
None
|
||||
} else {
|
||||
Some(base64_encode_url_safe!((page + 1).to_string()))
|
||||
};
|
||||
|
||||
Ok(CursorDataPage::new(
|
||||
next_page_token,
|
||||
page_size,
|
||||
total_items,
|
||||
Some(total_pages),
|
||||
items,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
MailerType::GmailApi => {
|
||||
let target_label = GmailLabels::get_by_name(account.id, mailbox_name).await?;
|
||||
let envelopes =
|
||||
GmailEnvelope::list_messages_in_label(target_label.id, page, page_size, desc)
|
||||
.await?;
|
||||
let DataPage {
|
||||
current_page: _,
|
||||
page_size,
|
||||
total_items,
|
||||
items,
|
||||
total_pages,
|
||||
} = GmailEnvelope::list_messages_in_label(target_label.id, page, page_size, desc)
|
||||
.await?;
|
||||
let map = GmailClient::label_map(account.id, account.use_proxy).await?;
|
||||
Ok(DataPage {
|
||||
current_page: envelopes.current_page,
|
||||
page_size: envelopes.page_size,
|
||||
total_items: envelopes.total_items,
|
||||
total_pages: envelopes.total_pages,
|
||||
items: envelopes
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|e| e.into_v3(&map))
|
||||
.collect(),
|
||||
})
|
||||
|
||||
if total_items == 0 {
|
||||
Ok(CursorDataPage::new(None, page_size, 0, None, vec![]))
|
||||
} else {
|
||||
let total_pages = total_pages.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"Internal error: total_pages is None (this should never happen)".into(),
|
||||
ErrorCode::InternalError
|
||||
)
|
||||
})?;
|
||||
|
||||
let next_page_token = if page == total_pages {
|
||||
None
|
||||
} else {
|
||||
Some(base64_encode_url_safe!((page + 1).to_string()))
|
||||
};
|
||||
|
||||
Ok(CursorDataPage::new(
|
||||
next_page_token,
|
||||
page_size,
|
||||
total_items,
|
||||
Some(total_pages),
|
||||
items.into_iter().map(|e| e.into_v3(&map)).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
// Licensed under RustMailer License Agreement v1.0
|
||||
// Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
use crate::base64_encode_url_safe;
|
||||
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::vendor::gmail::sync::client::GmailClient;
|
||||
use crate::modules::cache::vendor::gmail::sync::envelope::GmailEnvelope;
|
||||
use crate::modules::common::decode_page_token;
|
||||
use crate::modules::common::paginated::paginate_vec;
|
||||
use crate::modules::common::parallel::run_with_limit;
|
||||
use crate::modules::database::Paginated;
|
||||
use crate::modules::error::code::ErrorCode;
|
||||
use crate::modules::message::search::cache::IMAP_SEARCH_CACHE;
|
||||
use crate::modules::rest::response::CursorDataPage;
|
||||
use crate::{base64_decode_url_safe, base64_encode_url_safe};
|
||||
use crate::{
|
||||
encode_mailbox_name,
|
||||
modules::{
|
||||
@@ -611,22 +612,7 @@ impl MessageSearchRequest {
|
||||
));
|
||||
}
|
||||
|
||||
let page = match next_page_token {
|
||||
Some(next_page_token) => {
|
||||
let decoded = base64_decode_url_safe!(next_page_token)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
.and_then(|s| s.parse::<u64>().ok());
|
||||
|
||||
decoded.ok_or_else(|| {
|
||||
raise_error!(
|
||||
"Invalid next_page_token: not a valid page token".into(),
|
||||
ErrorCode::InvalidParameter
|
||||
)
|
||||
})?
|
||||
}
|
||||
None => 1,
|
||||
};
|
||||
let page = decode_page_token(next_page_token)?;
|
||||
let mailbox = self.mailbox.as_deref().ok_or_else(|| {
|
||||
raise_error!(
|
||||
"IMAP accounts must specify a mailbox (e.g. INBOX, Sent, or custom folder)".into(),
|
||||
|
||||
@@ -125,14 +125,17 @@ impl MessageApi {
|
||||
mailbox: Query<String>,
|
||||
/// fetches messages from the IMAP server; otherwise, uses local data.
|
||||
remote: Query<Option<bool>>,
|
||||
/// The page number for pagination (1-based).
|
||||
page: Query<u64>,
|
||||
/// The token for fetching the next page of results in pagination.
|
||||
///
|
||||
/// - If `None`, this indicates that the first page should be returned.
|
||||
/// - If `Some(token)`, the page corresponding to this token will be fetched.
|
||||
next_page_token: Query<Option<String>>,
|
||||
/// The number of messages per page.
|
||||
page_size: Query<u64>,
|
||||
/// lists messages in descending order; otherwise, ascending. internal date
|
||||
desc: Query<Option<bool>>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<Json<DataPage<EmailEnvelopeV3>>> {
|
||||
) -> ApiResult<Json<CursorDataPage<EmailEnvelopeV3>>> {
|
||||
let remote = remote.0.unwrap_or(false);
|
||||
let desc = desc.0.unwrap_or(false);
|
||||
let account_id = account_id.0;
|
||||
@@ -142,7 +145,7 @@ impl MessageApi {
|
||||
list_messages_in_mailbox(
|
||||
account_id,
|
||||
mailbox.0.trim(),
|
||||
page.0,
|
||||
next_page_token.0.as_deref(),
|
||||
page_size.0,
|
||||
remote,
|
||||
desc,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
export interface PaginatedResponse<S> {
|
||||
current_page: number | null;
|
||||
next_page_token: string | null;
|
||||
page_size: number | null;
|
||||
total_items: number;
|
||||
items: S[];
|
||||
|
||||
@@ -9,13 +9,39 @@ import axiosInstance from "@/api/axiosInstance";
|
||||
import { EmailEnvelope } from "@/features/mailbox/data/schema";
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
export const list_messages = async (accountId: number, mailbox: string, page: number, page_size: number, remote: boolean) => {
|
||||
const response = await axiosInstance.get<PaginatedResponse<EmailEnvelope>>(`/api/v1/list-messages/${accountId}?mailbox=${mailbox}&page=${page}&page_size=${page_size}&desc=true&remote=${remote}`);
|
||||
export const list_messages = async (accountId: number, mailbox: string, page_size: number, remote: boolean, next_page_token?: string) => {
|
||||
const params = new URLSearchParams({
|
||||
mailbox,
|
||||
page_size: String(page_size),
|
||||
desc: "true",
|
||||
remote: String(remote),
|
||||
});
|
||||
if (next_page_token) {
|
||||
params.append("next_page_token", next_page_token);
|
||||
}
|
||||
|
||||
const response = await axiosInstance.get<PaginatedResponse<EmailEnvelope>>(
|
||||
`/api/v1/list-messages/${accountId}?${params.toString()}`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const search_messages = async (accountId: number, page: number, page_size: number, remote: boolean, data: Record<string, any>) => {
|
||||
const response = await axiosInstance.post<PaginatedResponse<EmailEnvelope>>(`/api/v1/search-message/${accountId}?page=${page}&page_size=${page_size}&desc=true&remote=${remote}`, data);
|
||||
export const search_messages = async (accountId: number, page_size: number, remote: boolean, data: Record<string, any>, next_page_token?: string) => {
|
||||
const params = new URLSearchParams({
|
||||
page_size: String(page_size),
|
||||
desc: "true",
|
||||
remote: String(remote),
|
||||
});
|
||||
|
||||
if (next_page_token) {
|
||||
params.append("next_page_token", next_page_token);
|
||||
}
|
||||
|
||||
const response = await axiosInstance.post<PaginatedResponse<EmailEnvelope>>(
|
||||
`/api/v1/search-message/${accountId}?${params.toString()}`,
|
||||
data
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export function MailList({
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-1.5 p-1 sm:p-2">
|
||||
{items.map((item, index) => {
|
||||
{items.map((item) => {
|
||||
const isUnread = item.labels && item.labels.length > 0
|
||||
? gmail_unread(item)
|
||||
: !seen(item);
|
||||
@@ -77,7 +77,7 @@ export function MailList({
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
key={item.mid ?? item.uid}
|
||||
className={cn(
|
||||
"flex flex-col gap-1.5 p-2 rounded-lg border transition-all cursor-pointer",
|
||||
"hover:bg-accent/50",
|
||||
@@ -91,7 +91,7 @@ export function MailList({
|
||||
checked={selectedUids.includes(item.uid)}
|
||||
onCheckedChange={(checked) => handleCheckboxChange(checked, item.uid)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-3 w-3 shrink-0"
|
||||
className="h-4 w-3 shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1 min-w-0 flex-1">
|
||||
|
||||
@@ -40,6 +40,7 @@ import { EnvelopeDeleteDialog } from "./delete-dialog"
|
||||
import { useFlagMessageMutation } from "@/hooks/use-flag-messages"
|
||||
import { EnvelopeFilterDialog } from "./envelope-filter-dialog"
|
||||
import Logo from '@/assets/logo.svg'
|
||||
import { PaginatedResponse } from "@/api"
|
||||
|
||||
interface MailProps {
|
||||
defaultLayout: number[] | undefined
|
||||
@@ -51,7 +52,7 @@ interface MailProps {
|
||||
interface ListMessagesOptions {
|
||||
accountId: number | undefined;
|
||||
mailbox: string | undefined;
|
||||
page: number;
|
||||
next_page_token: string | undefined;
|
||||
page_size: number;
|
||||
remote: boolean;
|
||||
filter?: FilterForm;
|
||||
@@ -114,22 +115,44 @@ function buildPayload(filterForm: FilterForm): any {
|
||||
};
|
||||
}
|
||||
|
||||
const useListMessages = ({ accountId, mailbox, page, page_size, remote, filter }: ListMessagesOptions) => {
|
||||
return useQuery({
|
||||
queryKey: ['mailbox-list-messages', `${accountId}`, mailbox, page, page_size, remote, filter],
|
||||
queryFn: () => {
|
||||
if (filter) {
|
||||
const payload = {
|
||||
mailbox: mailbox!,
|
||||
search: buildPayload(filter)
|
||||
};
|
||||
return search_messages(accountId!, page, page_size, remote, payload);
|
||||
}
|
||||
return list_messages(accountId!, mailbox!, page, page_size, remote);
|
||||
},
|
||||
enabled: !!accountId && !!mailbox,
|
||||
});
|
||||
};
|
||||
// const useListMessages = ({ accountId, mailbox, page, page_size, remote, filter }: ListMessagesOptions) => {
|
||||
// return useQuery({
|
||||
// queryKey: ['mailbox-list-messages', `${accountId}`, mailbox, page, page_size, remote, filter],
|
||||
// queryFn: () => {
|
||||
// if (filter) {
|
||||
// const payload = {
|
||||
// mailbox: mailbox!,
|
||||
// search: buildPayload(filter)
|
||||
// };
|
||||
// return search_messages(accountId!, page, page_size, remote, payload);
|
||||
// }
|
||||
// return list_messages(accountId!, mailbox!, page, page_size, remote);
|
||||
// },
|
||||
// enabled: !!accountId && !!mailbox,
|
||||
// });
|
||||
// };
|
||||
|
||||
|
||||
export async function listMessagesAPI({
|
||||
accountId,
|
||||
mailbox,
|
||||
next_page_token,
|
||||
page_size,
|
||||
remote,
|
||||
filter
|
||||
}: ListMessagesOptions) {
|
||||
if (filter) {
|
||||
const payload = {
|
||||
mailbox,
|
||||
search: buildPayload(filter)
|
||||
};
|
||||
return await search_messages(accountId!, page_size, remote, payload, next_page_token);
|
||||
}
|
||||
|
||||
return await list_messages(accountId!, mailbox!, page_size, remote, next_page_token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function Mail({
|
||||
defaultLayout = [20, 80],
|
||||
@@ -143,6 +166,14 @@ export function Mail({
|
||||
const [selectedAccountId, setSelectedAccountId] = React.useState<number | undefined>(lastSelectedAccountId);
|
||||
const [selectedEvelope, setSelectedEvelope] = React.useState<EmailEnvelope | undefined>(undefined);
|
||||
const [useIMAP, setUseIMAP] = React.useState<boolean>(false);
|
||||
|
||||
const [envelopes, setEnvelopes] = React.useState<PaginatedResponse<EmailEnvelope> | undefined>(undefined);
|
||||
const [isMessagesLoading, setIsMessagesLoading] = React.useState<boolean>(false);
|
||||
const [isError, setIsError] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<any>(undefined);
|
||||
// const [pageTokenMap, setPageTokenMap] = React.useState<Record<number, string | undefined>>({});
|
||||
const pageTokenMapRef = React.useRef<Record<number, string | undefined>>({});
|
||||
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [selectedUids, setSelectedUids] = React.useState<number[]>([]);
|
||||
@@ -159,14 +190,48 @@ export function Mail({
|
||||
enabled: !!selectedAccountId,
|
||||
})
|
||||
|
||||
const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({
|
||||
accountId: selectedAccountId,
|
||||
mailbox: selectedMailbox?.name,
|
||||
page: page + 1,
|
||||
page_size: pageSize,
|
||||
remote: useIMAP,
|
||||
filter: currentFilter
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!selectedAccountId || !selectedMailbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
setIsMessagesLoading(true);
|
||||
setIsError(false);
|
||||
setError(undefined);
|
||||
const next_page_token = pageTokenMapRef.current[page];
|
||||
try {
|
||||
const data = await listMessagesAPI({
|
||||
accountId: selectedAccountId,
|
||||
mailbox: selectedMailbox?.name,
|
||||
page_size: pageSize,
|
||||
remote: useIMAP,
|
||||
filter: currentFilter,
|
||||
next_page_token
|
||||
});
|
||||
setEnvelopes(data);
|
||||
pageTokenMapRef.current = {
|
||||
...pageTokenMapRef.current,
|
||||
[page + 1]: data.next_page_token ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
setIsError(true);
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsMessagesLoading(false)
|
||||
}
|
||||
})();
|
||||
}, [selectedAccountId, selectedMailbox, page, pageSize, useIMAP, currentFilter])
|
||||
|
||||
// const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({
|
||||
// accountId: selectedAccountId,
|
||||
// mailbox: selectedMailbox?.name,
|
||||
// page: page + 1,
|
||||
// page_size: pageSize,
|
||||
// remote: useIMAP,
|
||||
// filter: currentFilter
|
||||
// });
|
||||
|
||||
const triggerUpdate = (mailbox: string) => {
|
||||
queryClient.refetchQueries({
|
||||
@@ -187,11 +252,8 @@ export function Mail({
|
||||
mailbox: selectedMailbox?.name,
|
||||
search: buildPayload(data)
|
||||
};
|
||||
const result = await search_messages(selectedAccountId, 1, 10, useIMAP, payload);
|
||||
queryClient.setQueryData(
|
||||
['mailbox-list-messages', selectedAccountId, selectedMailbox?.name, 1, 10, useIMAP, data],
|
||||
result
|
||||
);
|
||||
const result = await search_messages(selectedAccountId, 10, useIMAP, payload);
|
||||
setEnvelopes(result);
|
||||
} catch (error) {
|
||||
console.error('Error fetching messages:', error);
|
||||
toast({
|
||||
@@ -212,7 +274,10 @@ export function Mail({
|
||||
setSelectedUids([]);
|
||||
}
|
||||
|
||||
|
||||
const hasNextPage = () => {
|
||||
return !!pageTokenMapRef.current[page + 1];
|
||||
}
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
setSelectedUids([]);
|
||||
@@ -468,6 +533,7 @@ export function Mail({
|
||||
{selectedMailbox && <div className="flex justify-center mt-4">
|
||||
<EnvelopeListPagination
|
||||
totalItems={envelopes?.total_items ?? 0}
|
||||
hasNextPage={hasNextPage}
|
||||
pageIndex={page}
|
||||
pageSize={pageSize}
|
||||
setPageIndex={handlePageChange}
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -23,6 +21,7 @@ interface PaginationProps {
|
||||
totalItems: number
|
||||
pageIndex: number,
|
||||
pageSize: number,
|
||||
hasNextPage: () => boolean,
|
||||
setPageIndex: (pageIndex: number) => void,
|
||||
setPageSize: (pageSize: number) => void,
|
||||
}
|
||||
@@ -31,6 +30,7 @@ export function EnvelopeListPagination({
|
||||
totalItems,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
hasNextPage,
|
||||
setPageIndex,
|
||||
setPageSize,
|
||||
}: PaginationProps) {
|
||||
@@ -42,9 +42,9 @@ export function EnvelopeListPagination({
|
||||
setPageIndex(0) // Reset to the first page when page size changes
|
||||
}
|
||||
|
||||
const goToFirstPage = () => {
|
||||
setPageIndex(0)
|
||||
}
|
||||
// const goToFirstPage = () => {
|
||||
// setPageIndex(0)
|
||||
// }
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
const newPageIndex = Math.max(pageIndex - 1, 0)
|
||||
@@ -56,10 +56,10 @@ export function EnvelopeListPagination({
|
||||
setPageIndex(newPageIndex)
|
||||
}
|
||||
|
||||
const goToLastPage = () => {
|
||||
const newPageIndex = pageCount - 1
|
||||
setPageIndex(newPageIndex)
|
||||
}
|
||||
// const goToLastPage = () => {
|
||||
// const newPageIndex = pageCount - 1
|
||||
// setPageIndex(newPageIndex)
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between space-x-2 overflow-auto px-2'>
|
||||
@@ -89,7 +89,7 @@ export function EnvelopeListPagination({
|
||||
Page {pageIndex + 1} of {pageCount}
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant='outline'
|
||||
className='hidden h-8 w-8 p-0 lg:flex'
|
||||
onClick={goToFirstPage}
|
||||
@@ -97,7 +97,7 @@ export function EnvelopeListPagination({
|
||||
>
|
||||
<span className='sr-only'>Go to first page</span>
|
||||
<DoubleArrowLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-8 w-8 p-0'
|
||||
@@ -111,12 +111,12 @@ export function EnvelopeListPagination({
|
||||
variant='outline'
|
||||
className='h-8 w-8 p-0'
|
||||
onClick={goToNextPage}
|
||||
disabled={pageIndex === pageCount - 1}
|
||||
disabled={!hasNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to next page</span>
|
||||
<ChevronRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant='outline'
|
||||
className='hidden h-8 w-8 p-0 lg:flex'
|
||||
onClick={goToLastPage}
|
||||
@@ -124,7 +124,7 @@ export function EnvelopeListPagination({
|
||||
>
|
||||
<span className='sr-only'>Go to last page</span>
|
||||
<DoubleArrowRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,10 +231,11 @@ export default function License() {
|
||||
const FormSchema = z.object({
|
||||
license: z
|
||||
.string({ required_error: 'License key is required.' })
|
||||
.min(300, { message: "This is not a valid license key." })
|
||||
.max(500, { message: "This is not a valid license key." }),
|
||||
.min(300, { message: "License key is too short. Please check your license." })
|
||||
.max(800, { message: "License key is too long. Please check your license." }),
|
||||
});
|
||||
|
||||
|
||||
function UploadLicenseForm({ className, close }: React.ComponentProps<"form"> & { close: () => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const mutation = useMutation({
|
||||
|
||||
Reference in New Issue
Block a user