mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-11 21:15:45 +08:00
IMAP4rev1 Recent flag support
This commit is contained in:
parent
8f6ac2d114
commit
0168c1dca8
14 changed files with 265 additions and 87 deletions
|
@ -70,9 +70,15 @@ impl ImapResponse for Response {
|
||||||
}
|
}
|
||||||
buf.extend_from_slice(b"* ");
|
buf.extend_from_slice(b"* ");
|
||||||
buf.extend_from_slice(self.total_messages.to_string().as_bytes());
|
buf.extend_from_slice(self.total_messages.to_string().as_bytes());
|
||||||
|
if self.recent_messages > 0 {
|
||||||
|
buf.extend_from_slice(
|
||||||
|
b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\Recent)\r\n",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
buf.extend_from_slice(
|
buf.extend_from_slice(
|
||||||
b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n",
|
b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if self.is_rev2 {
|
if self.is_rev2 {
|
||||||
self.mailbox.serialize(&mut buf, self.is_rev2, false);
|
self.mailbox.serialize(&mut buf, self.is_rev2, false);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -36,10 +36,10 @@ use super::{SelectedMailbox, Session, SessionData, State, IMAP};
|
||||||
|
|
||||||
impl<T: AsyncRead> Session<T> {
|
impl<T: AsyncRead> Session<T> {
|
||||||
pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result<bool> {
|
pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result<bool> {
|
||||||
for line in String::from_utf8_lossy(bytes).split("\r\n") {
|
/*for line in String::from_utf8_lossy(bytes).split("\r\n") {
|
||||||
//let c = println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]);
|
//let c = println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]);
|
||||||
let c = println!("{}", line);
|
let c = println!("{}", line);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
tracing::trace!(parent: &self.span,
|
tracing::trace!(parent: &self.span,
|
||||||
event = "read",
|
event = "read",
|
||||||
|
@ -373,8 +373,16 @@ impl State {
|
||||||
matches!(self, State::Authenticated { .. } | State::Selected { .. })
|
matches!(self, State::Authenticated { .. } | State::Selected { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_mailbox_selected(&self) -> bool {
|
pub fn close_mailbox(&self) -> bool {
|
||||||
matches!(self, State::Selected { .. })
|
match self {
|
||||||
|
State::Selected { mailbox, data } => {
|
||||||
|
if mailbox.is_select {
|
||||||
|
data.clear_recent(&mailbox.id);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,14 @@ use jmap_proto::{
|
||||||
object::Object,
|
object::Object,
|
||||||
types::{collection::Collection, property::Property, value::Value},
|
types::{collection::Collection, property::Property, value::Value},
|
||||||
};
|
};
|
||||||
use store::write::{assert::HashedValue, BatchBuilder, F_VALUE};
|
use store::{
|
||||||
|
roaring::RoaringBitmap,
|
||||||
|
write::{assert::HashedValue, BatchBuilder, F_VALUE},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::core::ImapId;
|
use crate::core::ImapId;
|
||||||
|
|
||||||
use super::{MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData};
|
use super::{Mailbox, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData};
|
||||||
|
|
||||||
pub(crate) const MAX_RETRIES: usize = 10;
|
pub(crate) const MAX_RETRIES: usize = 10;
|
||||||
|
|
||||||
|
@ -107,7 +110,7 @@ impl SessionData {
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(message_ids.into_iter())
|
.zip(message_ids.iter())
|
||||||
{
|
{
|
||||||
// Make sure the message is still in this mailbox
|
// Make sure the message is still in this mailbox
|
||||||
if let Some(uid_mailbox) = uid_mailbox {
|
if let Some(uid_mailbox) = uid_mailbox {
|
||||||
|
@ -137,6 +140,7 @@ impl SessionData {
|
||||||
let mut try_count = 0;
|
let mut try_count = 0;
|
||||||
let mut uid_next = 1;
|
let mut uid_next = 1;
|
||||||
let mut uid_other = 0;
|
let mut uid_other = 0;
|
||||||
|
let mut recent_messages = RoaringBitmap::new();
|
||||||
|
|
||||||
// Shuffle unassigned
|
// Shuffle unassigned
|
||||||
/*if unassigned.len() > 1 {
|
/*if unassigned.len() > 1 {
|
||||||
|
@ -231,6 +235,7 @@ impl SessionData {
|
||||||
message_id = message_id,
|
message_id = message_id,
|
||||||
"Duplicate UID");
|
"Duplicate UID");
|
||||||
}
|
}
|
||||||
|
recent_messages.insert(message_id);
|
||||||
}
|
}
|
||||||
Err(store::Error::AssertValueFailed)
|
Err(store::Error::AssertValueFailed)
|
||||||
if try_count < MAX_RETRIES =>
|
if try_count < MAX_RETRIES =>
|
||||||
|
@ -309,6 +314,21 @@ impl SessionData {
|
||||||
uid_to_id.insert(uid, message_id);
|
uid_to_id.insert(uid, message_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update recent flags
|
||||||
|
for account in self.mailboxes.lock().iter_mut() {
|
||||||
|
if account.account_id == mailbox.account_id {
|
||||||
|
let mailbox = account
|
||||||
|
.mailbox_state
|
||||||
|
.entry(mailbox.mailbox_id)
|
||||||
|
.or_insert_with(Mailbox::default);
|
||||||
|
mailbox.recent_messages &= &message_ids;
|
||||||
|
if !recent_messages.is_empty() {
|
||||||
|
mailbox.recent_messages |= &recent_messages;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(MailboxState {
|
Ok(MailboxState {
|
||||||
uid_next,
|
uid_next,
|
||||||
uid_validity,
|
uid_validity,
|
||||||
|
@ -424,6 +444,38 @@ impl SessionData {
|
||||||
Err(StatusResponse::database_failure())
|
Err(StatusResponse::database_failure())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_recent(&self, mailbox: &MailboxId) -> RoaringBitmap {
|
||||||
|
for account in self.mailboxes.lock().iter() {
|
||||||
|
if account.account_id == mailbox.account_id {
|
||||||
|
if let Some(mailbox) = account.mailbox_state.get(&mailbox.mailbox_id) {
|
||||||
|
return mailbox.recent_messages.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RoaringBitmap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_recent_count(&self, mailbox: &MailboxId) -> usize {
|
||||||
|
for account in self.mailboxes.lock().iter() {
|
||||||
|
if account.account_id == mailbox.account_id {
|
||||||
|
if let Some(mailbox) = account.mailbox_state.get(&mailbox.mailbox_id) {
|
||||||
|
return mailbox.recent_messages.len() as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_recent(&self, mailbox: &MailboxId) {
|
||||||
|
for account in self.mailboxes.lock().iter_mut() {
|
||||||
|
if account.account_id == mailbox.account_id {
|
||||||
|
if let Some(mailbox) = account.mailbox_state.get_mut(&mailbox.mailbox_id) {
|
||||||
|
mailbox.recent_messages.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectedMailbox {
|
impl SelectedMailbox {
|
||||||
|
|
|
@ -42,6 +42,7 @@ use jmap::{
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use store::roaring::RoaringBitmap;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncRead, ReadHalf},
|
io::{AsyncRead, ReadHalf},
|
||||||
sync::{mpsc, watch},
|
sync::{mpsc, watch},
|
||||||
|
@ -127,6 +128,7 @@ pub struct Mailbox {
|
||||||
pub uid_validity: Option<u32>,
|
pub uid_validity: Option<u32>,
|
||||||
pub uid_next: Option<u32>,
|
pub uid_next: Option<u32>,
|
||||||
pub size: Option<u32>,
|
pub size: Option<u32>,
|
||||||
|
pub recent_messages: RoaringBitmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -58,7 +58,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender<Even
|
||||||
size = bytes.len()
|
size = bytes.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let c = print!("{}", String::from_utf8_lossy(&bytes));
|
//let c = print!("{}", String::from_utf8_lossy(&bytes));
|
||||||
|
|
||||||
match stream_tx.write_all(bytes.as_ref()).await {
|
match stream_tx.write_all(bytes.as_ref()).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
@ -101,7 +101,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender<Even
|
||||||
size = bytes.len()
|
size = bytes.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let c = print!("{}", String::from_utf8_lossy(&bytes));
|
//let c = print!("{}", String::from_utf8_lossy(&bytes));
|
||||||
|
|
||||||
match stream_tx.write_all(bytes.as_ref()).await {
|
match stream_tx.write_all(bytes.as_ref()).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
@ -132,10 +132,10 @@ impl<T: AsyncRead> Session<T> {
|
||||||
pub async fn write_bytes(&self, bytes: impl Into<Cow<'static, [u8]>>) -> crate::OpResult {
|
pub async fn write_bytes(&self, bytes: impl Into<Cow<'static, [u8]>>) -> crate::OpResult {
|
||||||
let bytes = bytes.into();
|
let bytes = bytes.into();
|
||||||
|
|
||||||
let c = println!(
|
/*let c = println!(
|
||||||
"{:?}",
|
"-> {:?}",
|
||||||
String::from_utf8_lossy(&bytes[..std::cmp::min(bytes.len(), 100)])
|
String::from_utf8_lossy(&bytes[..std::cmp::min(bytes.len(), 100)])
|
||||||
);
|
);*/
|
||||||
|
|
||||||
if let Err(err) = self.writer.send(Event::Bytes(bytes)).await {
|
if let Err(err) = self.writer.send(Event::Bytes(bytes)).await {
|
||||||
debug!("Failed to send bytes: {}", err);
|
debug!("Failed to send bytes: {}", err);
|
||||||
|
@ -149,10 +149,10 @@ impl<T: AsyncRead> Session<T> {
|
||||||
impl SessionData {
|
impl SessionData {
|
||||||
pub async fn write_bytes(&self, bytes: impl Into<Cow<'static, [u8]>>) -> bool {
|
pub async fn write_bytes(&self, bytes: impl Into<Cow<'static, [u8]>>) -> bool {
|
||||||
let bytes = bytes.into();
|
let bytes = bytes.into();
|
||||||
let c = println!(
|
/*let c = println!(
|
||||||
"{:?}",
|
"-> {:?}",
|
||||||
String::from_utf8_lossy(&bytes[..std::cmp::min(bytes.len(), 100)])
|
String::from_utf8_lossy(&bytes[..std::cmp::min(bytes.len(), 100)])
|
||||||
);
|
);*/
|
||||||
|
|
||||||
if let Err(err) = self.writer.send(Event::Bytes(bytes)).await {
|
if let Err(err) = self.writer.send(Event::Bytes(bytes)).await {
|
||||||
debug!("Failed to send bytes: {}", err);
|
debug!("Failed to send bytes: {}", err);
|
||||||
|
|
|
@ -34,7 +34,7 @@ use jmap_proto::{
|
||||||
type_state::DataType, value::Value,
|
type_state::DataType, value::Value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use store::{query::Filter, write::BatchBuilder};
|
use store::{query::Filter, roaring::RoaringBitmap, write::BatchBuilder};
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
use crate::core::{Account, Mailbox, Session, SessionData};
|
use crate::core::{Account, Mailbox, Session, SessionData};
|
||||||
|
@ -217,6 +217,7 @@ impl SessionData {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
recent_messages: RoaringBitmap::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ impl<T: AsyncRead> Session<T> {
|
||||||
Ok(arguments) => {
|
Ok(arguments) => {
|
||||||
let (data, mailbox) = self.state.select_data();
|
let (data, mailbox) = self.state.select_data();
|
||||||
let is_qresync = self.is_qresync;
|
let is_qresync = self.is_qresync;
|
||||||
|
let is_rev2 = self.version.is_rev2();
|
||||||
|
|
||||||
let enabled_condstore = if !self.is_condstore && arguments.changed_since.is_some()
|
let enabled_condstore = if !self.is_condstore && arguments.changed_since.is_some()
|
||||||
|| arguments.attributes.contains(&Attribute::ModSeq)
|
|| arguments.attributes.contains(&Attribute::ModSeq)
|
||||||
|
@ -78,7 +79,14 @@ impl<T: AsyncRead> Session<T> {
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
data.write_bytes(
|
data.write_bytes(
|
||||||
data.fetch(arguments, mailbox, is_uid, is_qresync, enabled_condstore)
|
data.fetch(
|
||||||
|
arguments,
|
||||||
|
mailbox,
|
||||||
|
is_uid,
|
||||||
|
is_qresync,
|
||||||
|
is_rev2,
|
||||||
|
enabled_condstore,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.into_bytes(),
|
.into_bytes(),
|
||||||
)
|
)
|
||||||
|
@ -98,6 +106,7 @@ impl SessionData {
|
||||||
mailbox: Arc<SelectedMailbox>,
|
mailbox: Arc<SelectedMailbox>,
|
||||||
is_uid: bool,
|
is_uid: bool,
|
||||||
is_qresync: bool,
|
is_qresync: bool,
|
||||||
|
is_rev2: bool,
|
||||||
enabled_condstore: bool,
|
enabled_condstore: bool,
|
||||||
) -> StatusResponse {
|
) -> StatusResponse {
|
||||||
// Validate VANISHED parameter
|
// Validate VANISHED parameter
|
||||||
|
@ -117,6 +126,11 @@ impl SessionData {
|
||||||
Ok(modseq) => modseq,
|
Ok(modseq) => modseq,
|
||||||
Err(response) => return response.with_tag(arguments.tag),
|
Err(response) => return response.with_tag(arguments.tag),
|
||||||
};
|
};
|
||||||
|
let recent_messages = if !is_rev2 {
|
||||||
|
self.get_recent(&mailbox.id).into()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Convert IMAP ids to JMAP ids.
|
// Convert IMAP ids to JMAP ids.
|
||||||
let mut ids = match mailbox
|
let mut ids = match mailbox
|
||||||
|
@ -353,6 +367,12 @@ impl SessionData {
|
||||||
if set_seen_flag {
|
if set_seen_flag {
|
||||||
flags.push(Flag::Seen);
|
flags.push(Flag::Seen);
|
||||||
}
|
}
|
||||||
|
if recent_messages
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |recent| recent.contains(id))
|
||||||
|
{
|
||||||
|
flags.push(Flag::Recent);
|
||||||
|
}
|
||||||
items.push(DataItem::Flags { flags });
|
items.push(DataItem::Flags { flags });
|
||||||
}
|
}
|
||||||
Attribute::InternalDate => {
|
Attribute::InternalDate => {
|
||||||
|
|
|
@ -268,6 +268,7 @@ impl SessionData {
|
||||||
mailbox.clone(),
|
mailbox.clone(),
|
||||||
true,
|
true,
|
||||||
is_qresync,
|
is_qresync,
|
||||||
|
is_rev2,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -574,17 +574,11 @@ impl SessionData {
|
||||||
filters.push(query::Filter::End);
|
filters.push(query::Filter::End);
|
||||||
}
|
}
|
||||||
search::Filter::Recent => {
|
search::Filter::Recent => {
|
||||||
filters.push(query::Filter::is_in_bitmap(
|
filters.push(query::Filter::is_in_set(self.get_recent(&mailbox.id)));
|
||||||
Property::Keywords,
|
|
||||||
Keyword::Recent,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
search::Filter::New => {
|
search::Filter::New => {
|
||||||
filters.push(query::Filter::And);
|
filters.push(query::Filter::And);
|
||||||
filters.push(query::Filter::is_in_bitmap(
|
filters.push(query::Filter::is_in_set(self.get_recent(&mailbox.id)));
|
||||||
Property::Keywords,
|
|
||||||
Keyword::Recent,
|
|
||||||
));
|
|
||||||
filters.push(query::Filter::Not);
|
filters.push(query::Filter::Not);
|
||||||
filters.push(query::Filter::is_in_bitmap(
|
filters.push(query::Filter::is_in_bitmap(
|
||||||
Property::Keywords,
|
Property::Keywords,
|
||||||
|
@ -595,10 +589,7 @@ impl SessionData {
|
||||||
}
|
}
|
||||||
search::Filter::Old => {
|
search::Filter::Old => {
|
||||||
filters.push(query::Filter::Not);
|
filters.push(query::Filter::Not);
|
||||||
filters.push(query::Filter::is_in_bitmap(
|
filters.push(query::Filter::is_in_set(self.get_recent(&mailbox.id)));
|
||||||
Property::Keywords,
|
|
||||||
Keyword::Seen,
|
|
||||||
));
|
|
||||||
filters.push(query::Filter::End);
|
filters.push(query::Filter::End);
|
||||||
}
|
}
|
||||||
search::Filter::Older(secs) => {
|
search::Filter::Older(secs) => {
|
||||||
|
|
|
@ -52,13 +52,14 @@ impl<T: AsyncRead> Session<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) {
|
if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) {
|
||||||
// Syncronize messages
|
// Synchronize messages
|
||||||
match data.fetch_messages(&mailbox).await {
|
match data.fetch_messages(&mailbox).await {
|
||||||
Ok(state) => {
|
Ok(state) => {
|
||||||
let closed_previous = self.state.is_mailbox_selected();
|
let closed_previous = self.state.close_mailbox();
|
||||||
let is_condstore = self.is_condstore || arguments.condstore;
|
let is_condstore = self.is_condstore || arguments.condstore;
|
||||||
|
|
||||||
// Build new state
|
// Build new state
|
||||||
|
let is_rev2 = self.version.is_rev2();
|
||||||
let uid_validity = state.uid_validity;
|
let uid_validity = state.uid_validity;
|
||||||
let uid_next = state.uid_next;
|
let uid_next = state.uid_next;
|
||||||
let total_messages = state.total_messages;
|
let total_messages = state.total_messages;
|
||||||
|
@ -105,6 +106,7 @@ impl<T: AsyncRead> Session<T> {
|
||||||
mailbox.clone(),
|
mailbox.clone(),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
is_rev2,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -115,12 +117,12 @@ impl<T: AsyncRead> Session<T> {
|
||||||
let response = Response {
|
let response = Response {
|
||||||
mailbox: ListItem::new(arguments.mailbox_name),
|
mailbox: ListItem::new(arguments.mailbox_name),
|
||||||
total_messages,
|
total_messages,
|
||||||
recent_messages: 0,
|
recent_messages: data.get_recent_count(&mailbox.id),
|
||||||
unseen_seq: 0,
|
unseen_seq: 0,
|
||||||
uid_validity,
|
uid_validity,
|
||||||
uid_next,
|
uid_next,
|
||||||
closed_previous,
|
closed_previous,
|
||||||
is_rev2: self.version.is_rev2(),
|
is_rev2,
|
||||||
highest_modseq,
|
highest_modseq,
|
||||||
mailbox_id: Id::from_parts(
|
mailbox_id: Id::from_parts(
|
||||||
mailbox.id.account_id,
|
mailbox.id.account_id,
|
||||||
|
@ -164,6 +166,7 @@ impl<T: AsyncRead> Session<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_unselect(&mut self, request: Request<Command>) -> crate::OpResult {
|
pub async fn handle_unselect(&mut self, request: Request<Command>) -> crate::OpResult {
|
||||||
|
self.state.close_mailbox();
|
||||||
self.state = State::Authenticated {
|
self.state = State::Authenticated {
|
||||||
data: self.state.session_data(),
|
data: self.state.session_data(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,10 @@ use imap_proto::{
|
||||||
receiver::Request,
|
receiver::Request,
|
||||||
Command, ResponseCode, StatusResponse,
|
Command, ResponseCode, StatusResponse,
|
||||||
};
|
};
|
||||||
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
|
use jmap_proto::{
|
||||||
|
object::Object,
|
||||||
|
types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value},
|
||||||
|
};
|
||||||
use store::roaring::RoaringBitmap;
|
use store::roaring::RoaringBitmap;
|
||||||
use store::Deserialize;
|
use store::Deserialize;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
|
@ -127,7 +130,6 @@ impl SessionData {
|
||||||
// Make sure all requested fields are up to date
|
// Make sure all requested fields are up to date
|
||||||
let mut items_update = Vec::with_capacity(items.len());
|
let mut items_update = Vec::with_capacity(items.len());
|
||||||
let mut items_response = Vec::with_capacity(items.len());
|
let mut items_response = Vec::with_capacity(items.len());
|
||||||
let mut do_synchronize = false;
|
|
||||||
|
|
||||||
for account in self.mailboxes.lock().iter_mut() {
|
for account in self.mailboxes.lock().iter_mut() {
|
||||||
if account.account_id == mailbox.account_id {
|
if account.account_id == mailbox.account_id {
|
||||||
|
@ -135,6 +137,7 @@ impl SessionData {
|
||||||
.mailbox_state
|
.mailbox_state
|
||||||
.entry(mailbox.mailbox_id)
|
.entry(mailbox.mailbox_id)
|
||||||
.or_insert_with(Mailbox::default);
|
.or_insert_with(Mailbox::default);
|
||||||
|
let update_recent = mailbox_state.total_messages.is_none();
|
||||||
for item in items {
|
for item in items {
|
||||||
match item {
|
match item {
|
||||||
Status::Messages => {
|
Status::Messages => {
|
||||||
|
@ -149,7 +152,6 @@ impl SessionData {
|
||||||
items_response.push((*item, StatusItemType::Number(value as u64)));
|
items_response.push((*item, StatusItemType::Number(value as u64)));
|
||||||
} else {
|
} else {
|
||||||
items_update.push_unique(*item);
|
items_update.push_unique(*item);
|
||||||
do_synchronize = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Status::UidValidity => {
|
Status::UidValidity => {
|
||||||
|
@ -157,7 +159,6 @@ impl SessionData {
|
||||||
items_response.push((*item, StatusItemType::Number(value as u64)));
|
items_response.push((*item, StatusItemType::Number(value as u64)));
|
||||||
} else {
|
} else {
|
||||||
items_update.push_unique(*item);
|
items_update.push_unique(*item);
|
||||||
do_synchronize = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Status::Unseen => {
|
Status::Unseen => {
|
||||||
|
@ -197,7 +198,14 @@ impl SessionData {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Status::Recent => {
|
Status::Recent => {
|
||||||
items_response.push((*item, StatusItemType::Number(0)));
|
if !update_recent {
|
||||||
|
items_response.push((
|
||||||
|
*item,
|
||||||
|
StatusItemType::Number(mailbox_state.recent_messages.len()),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
items_update.push_unique(*item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,12 +216,6 @@ impl SessionData {
|
||||||
if !items_update.is_empty() {
|
if !items_update.is_empty() {
|
||||||
// Retrieve latest values
|
// Retrieve latest values
|
||||||
let mut values_update = Vec::with_capacity(items_update.len());
|
let mut values_update = Vec::with_capacity(items_update.len());
|
||||||
let mailbox_state = if do_synchronize {
|
|
||||||
self.fetch_messages(&mailbox).await?.into()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mailbox_message_ids = self
|
let mailbox_message_ids = self
|
||||||
.jmap
|
.jmap
|
||||||
.get_tag(
|
.get_tag(
|
||||||
|
@ -232,8 +234,38 @@ impl SessionData {
|
||||||
for item in items_update {
|
for item in items_update {
|
||||||
let result = match item {
|
let result = match item {
|
||||||
Status::Messages => mailbox_message_ids.as_ref().map(|v| v.len()).unwrap_or(0),
|
Status::Messages => mailbox_message_ids.as_ref().map(|v| v.len()).unwrap_or(0),
|
||||||
Status::UidNext => mailbox_state.as_ref().unwrap().uid_next as u64,
|
Status::UidNext => {
|
||||||
Status::UidValidity => mailbox_state.as_ref().unwrap().uid_validity as u64,
|
(self
|
||||||
|
.jmap
|
||||||
|
.get_property::<u32>(
|
||||||
|
mailbox.account_id,
|
||||||
|
Collection::Mailbox,
|
||||||
|
mailbox.mailbox_id,
|
||||||
|
Property::EmailIds,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap_or(0)
|
||||||
|
+ 1) as u64
|
||||||
|
}
|
||||||
|
Status::UidValidity => self
|
||||||
|
.jmap
|
||||||
|
.get_property::<Object<Value>>(
|
||||||
|
mailbox.account_id,
|
||||||
|
Collection::Mailbox,
|
||||||
|
mailbox.mailbox_id,
|
||||||
|
&Property::Value,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.and_then(|obj| obj.get(&Property::Cid).as_uint())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
tracing::debug!(event = "error",
|
||||||
|
context = "store",
|
||||||
|
account_id = mailbox.account_id,
|
||||||
|
collection = ?Collection::Mailbox,
|
||||||
|
mailbox_id = mailbox.mailbox_id,
|
||||||
|
"Failed to obtain uid validity");
|
||||||
|
StatusResponse::no("Mailbox unavailable.")
|
||||||
|
})?,
|
||||||
Status::Unseen => {
|
Status::Unseen => {
|
||||||
if let (Some(message_ids), Some(mailbox_message_ids)) =
|
if let (Some(message_ids), Some(mailbox_message_ids)) =
|
||||||
(&message_ids, &mailbox_message_ids)
|
(&message_ids, &mailbox_message_ids)
|
||||||
|
@ -284,7 +316,11 @@ impl SessionData {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Status::HighestModSeq | Status::MailboxId | Status::Recent => {
|
Status::Recent => {
|
||||||
|
self.fetch_messages(&mailbox).await?;
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Status::HighestModSeq | Status::MailboxId => {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -309,7 +345,15 @@ impl SessionData {
|
||||||
Status::Unseen => mailbox_state.total_unseen = value.into(),
|
Status::Unseen => mailbox_state.total_unseen = value.into(),
|
||||||
Status::Deleted => mailbox_state.total_deleted = value.into(),
|
Status::Deleted => mailbox_state.total_deleted = value.into(),
|
||||||
Status::Size => mailbox_state.size = value.into(),
|
Status::Size => mailbox_state.size = value.into(),
|
||||||
Status::HighestModSeq | Status::MailboxId | Status::Recent => {
|
Status::Recent => {
|
||||||
|
items_response
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(i, _)| *i == Status::Recent)
|
||||||
|
.unwrap()
|
||||||
|
.1 =
|
||||||
|
StatusItemType::Number(mailbox_state.recent_messages.len());
|
||||||
|
}
|
||||||
|
Status::HighestModSeq | Status::MailboxId => {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,59 +25,102 @@ use imap_proto::ResponseType;
|
||||||
|
|
||||||
use super::{AssertResult, ImapConnection, Type};
|
use super::{AssertResult, ImapConnection, Type};
|
||||||
|
|
||||||
pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
pub async fn test(_imap: &mut ImapConnection, imap_check: &mut ImapConnection) {
|
||||||
// Check status
|
// Check status
|
||||||
imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
imap_check
|
||||||
|
.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE RECENT))")
|
||||||
.await;
|
.await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)");
|
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193 RECENT 0)");
|
||||||
|
|
||||||
// Select INBOX
|
// Select INBOX
|
||||||
imap.send("SELECT INBOX").await;
|
imap_check.send("SELECT INBOX").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
|
|
||||||
// Copying to the same mailbox should fail
|
// Copying to the same mailbox should fail
|
||||||
imap.send("COPY 1:* INBOX").await;
|
imap_check.send("COPY 1:* INBOX").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::No)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::No)
|
||||||
.await
|
.await
|
||||||
.assert_response_code("CANNOT");
|
.assert_response_code("CANNOT");
|
||||||
|
|
||||||
// Copying to a non-existent mailbox should fail
|
// Copying to a non-existent mailbox should fail
|
||||||
imap.send("COPY 1:* \"/dev/null\"").await;
|
imap_check.send("COPY 1:* \"/dev/null\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::No)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::No)
|
||||||
.await
|
.await
|
||||||
.assert_response_code("TRYCREATE");
|
.assert_response_code("TRYCREATE");
|
||||||
|
|
||||||
// Create test folders
|
// Create test folders
|
||||||
imap.send("CREATE \"Scamorza Affumicata\"").await;
|
imap_check.send("CREATE \"Scamorza Affumicata\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
imap.send("CREATE \"Burrata al Tartufo\"").await;
|
imap_check.send("CREATE \"Burrata al Tartufo\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
|
|
||||||
// Copy messages
|
// Copy messages
|
||||||
imap.send("COPY 1,3,5,7 \"Scamorza Affumicata\"").await;
|
imap_check
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
.send("COPY 1,3,5,7 \"Scamorza Affumicata\"")
|
||||||
|
.await;
|
||||||
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("COPYUID")
|
.assert_contains("COPYUID")
|
||||||
.assert_contains("1:4");
|
.assert_contains("1:4");
|
||||||
|
|
||||||
// Check status
|
// Check status
|
||||||
imap.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE)")
|
imap_check
|
||||||
|
.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE RECENT)")
|
||||||
.await;
|
.await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("MESSAGES 4")
|
.assert_contains("MESSAGES 4")
|
||||||
|
.assert_contains("RECENT 4")
|
||||||
.assert_contains("UNSEEN 4")
|
.assert_contains("UNSEEN 4")
|
||||||
.assert_contains("UIDNEXT 5")
|
.assert_contains("UIDNEXT 5")
|
||||||
.assert_contains("SIZE 5851");
|
.assert_contains("SIZE 5851");
|
||||||
|
|
||||||
// Move all messages to Burrata
|
// Check \Recent flag
|
||||||
imap.send("SELECT \"Scamorza Affumicata\"").await;
|
imap_check.send("SELECT \"Scamorza Affumicata\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_contains("* 4 RECENT");
|
||||||
|
imap_check.send("FETCH 1:* (UID FLAGS)").await;
|
||||||
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_count("\\Recent", 4);
|
||||||
|
imap_check.send("UNSELECT").await;
|
||||||
|
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
|
imap_check
|
||||||
|
.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE RECENT)")
|
||||||
|
.await;
|
||||||
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_contains("MESSAGES 4")
|
||||||
|
.assert_contains("RECENT 0")
|
||||||
|
.assert_contains("UNSEEN 4")
|
||||||
|
.assert_contains("UIDNEXT 5")
|
||||||
|
.assert_contains("SIZE 5851");
|
||||||
|
imap_check.send("SELECT \"Scamorza Affumicata\"").await;
|
||||||
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_contains("* 0 RECENT");
|
||||||
|
imap_check.send("FETCH 1:* (UID FLAGS)").await;
|
||||||
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
.await
|
||||||
|
.assert_count("\\Recent", 0);
|
||||||
|
|
||||||
imap.send("MOVE 1:* \"Burrata al Tartufo\"").await;
|
// Move all messages to Burrata
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap_check.send("MOVE 1:* \"Burrata al Tartufo\"").await;
|
||||||
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("* OK [COPYUID")
|
.assert_contains("* OK [COPYUID")
|
||||||
.assert_contains("1:4")
|
.assert_contains("1:4")
|
||||||
|
@ -87,20 +130,23 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||||
.assert_contains("* 1 EXPUNGE");
|
.assert_contains("* 1 EXPUNGE");
|
||||||
|
|
||||||
// Check status
|
// Check status
|
||||||
imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
imap_check
|
||||||
|
.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
||||||
.await;
|
.await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 4 UNSEEN 4 SIZE 5851)")
|
.assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 4 UNSEEN 4 SIZE 5851)")
|
||||||
.assert_contains("\"Scamorza Affumicata\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)")
|
.assert_contains("\"Scamorza Affumicata\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)")
|
||||||
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)");
|
.assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)");
|
||||||
|
|
||||||
// Move the messages back to Scamorza, UIDNEXT should increase.
|
// Move the messages back to Scamorza, UIDNEXT should increase.
|
||||||
imap.send("SELECT \"Burrata al Tartufo\"").await;
|
imap_check.send("SELECT \"Burrata al Tartufo\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).await;
|
imap_check.assert_read(Type::Tagged, ResponseType::Ok).await;
|
||||||
|
|
||||||
imap.send("MOVE 1:* \"Scamorza Affumicata\"").await;
|
imap_check.send("MOVE 1:* \"Scamorza Affumicata\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("* OK [COPYUID")
|
.assert_contains("* OK [COPYUID")
|
||||||
.assert_contains("5:8")
|
.assert_contains("5:8")
|
||||||
|
@ -110,9 +156,11 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||||
.assert_contains("* 1 EXPUNGE");
|
.assert_contains("* 1 EXPUNGE");
|
||||||
|
|
||||||
// Check status
|
// Check status
|
||||||
imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
imap_check
|
||||||
|
.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))")
|
||||||
.await;
|
.await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap_check
|
||||||
|
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
.await
|
.await
|
||||||
.assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)")
|
.assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)")
|
||||||
.assert_contains("\"Scamorza Affumicata\" (UIDNEXT 9 MESSAGES 4 UNSEEN 4 SIZE 5851)")
|
.assert_contains("\"Scamorza Affumicata\" (UIDNEXT 9 MESSAGES 4 UNSEEN 4 SIZE 5851)")
|
||||||
|
|
|
@ -144,7 +144,8 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
|
||||||
.assert_contains("* 7 FETCH (UID 7 ")
|
.assert_contains("* 7 FETCH (UID 7 ")
|
||||||
.assert_contains("* 8 FETCH (UID 8 ")
|
.assert_contains("* 8 FETCH (UID 8 ")
|
||||||
.assert_contains("* 9 FETCH (UID 9 ")
|
.assert_contains("* 9 FETCH (UID 9 ")
|
||||||
.assert_contains("* 10 FETCH (UID 10 ");
|
.assert_contains("* 10 FETCH (UID 10 ")
|
||||||
|
.assert_count("\\Recent", 0);
|
||||||
|
|
||||||
imap.send("FETCH 7:* (UID FLAGS)").await;
|
imap.send("FETCH 7:* (UID FLAGS)").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||||
|
|
|
@ -560,10 +560,11 @@ impl AssertResult for Vec<String> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.iter().filter(|l| l.contains(text)).count(),
|
self.iter().filter(|l| l.contains(text)).count(),
|
||||||
occurrences,
|
occurrences,
|
||||||
"Expected {} occurrences of {:?}, found {}.",
|
"Expected {} occurrences of {:?}, found {} in {:?}.",
|
||||||
occurrences,
|
occurrences,
|
||||||
text,
|
text,
|
||||||
self.iter().filter(|l| l.contains(text)).count()
|
self.iter().filter(|l| l.contains(text)).count(),
|
||||||
|
self
|
||||||
);
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue