mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-25 00:36:41 +00:00
Email/set create tests passing
This commit is contained in:
parent
46b5dc0425
commit
c3a2b43ddb
31 changed files with 2257 additions and 310 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
/Cargo.lock
|
||||
.vscode
|
||||
*.failed
|
||||
*_failed
|
||||
|
|
|
@ -34,13 +34,19 @@ pub struct SetError {
|
|||
description: Option<Cow<'static, str>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
properties: Option<Vec<Property>>,
|
||||
properties: Option<Vec<InvalidProperty>>,
|
||||
|
||||
#[serde(rename = "existingId")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
existing_id: Option<Id>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InvalidProperty {
|
||||
Property(Property),
|
||||
Path(Vec<Property>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub enum SetErrorType {
|
||||
#[serde(rename = "forbidden")]
|
||||
|
@ -142,13 +148,20 @@ impl SetError {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_property(mut self, property: Property) -> Self {
|
||||
self.properties = vec![property].into();
|
||||
pub fn with_property(mut self, property: impl Into<InvalidProperty>) -> Self {
|
||||
self.properties = vec![property.into()].into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_properties(mut self, properties: impl IntoIterator<Item = Property>) -> Self {
|
||||
self.properties = properties.into_iter().collect::<Vec<_>>().into();
|
||||
pub fn with_properties(
|
||||
mut self,
|
||||
properties: impl IntoIterator<Item = impl Into<InvalidProperty>>,
|
||||
) -> Self {
|
||||
self.properties = properties
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -165,9 +178,49 @@ impl SetError {
|
|||
Self::new(SetErrorType::Forbidden)
|
||||
}
|
||||
|
||||
pub fn not_found() -> Self {
|
||||
Self::new(SetErrorType::NotFound)
|
||||
}
|
||||
|
||||
pub fn already_exists() -> Self {
|
||||
Self::new(SetErrorType::AlreadyExists)
|
||||
}
|
||||
|
||||
pub fn will_destroy() -> Self {
|
||||
Self::new(SetErrorType::WillDestroy).with_description("ID will be destroyed.")
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, SetError>;
|
||||
impl From<Property> for InvalidProperty {
|
||||
fn from(property: Property) -> Self {
|
||||
InvalidProperty::Property(property)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Property, Property)> for InvalidProperty {
|
||||
fn from((a, b): (Property, Property)) -> Self {
|
||||
InvalidProperty::Path(vec![a, b])
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for InvalidProperty {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
InvalidProperty::Property(p) => p.serialize(serializer),
|
||||
InvalidProperty::Path(p) => {
|
||||
use std::fmt::Write;
|
||||
let mut path = String::with_capacity(64);
|
||||
for (i, p) in p.iter().enumerate() {
|
||||
if i > 0 {
|
||||
path.push('/');
|
||||
}
|
||||
let _ = write!(path, "{}", p);
|
||||
}
|
||||
path.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,108 +5,3 @@ pub mod parser;
|
|||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod types;
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
#[test]
|
||||
fn gen_hash() {
|
||||
//let mut table = BTreeMap::new();
|
||||
for value in ["blobIds", "ifInState", "emails"] {
|
||||
let mut hash = 0;
|
||||
let mut shift = 0;
|
||||
let lower_first = false;
|
||||
|
||||
for (pos, &ch) in value.as_bytes().iter().take(16).enumerate() {
|
||||
if pos == 0 && lower_first {
|
||||
hash |= (ch.to_ascii_lowercase() as u128) << shift;
|
||||
} else {
|
||||
hash |= (ch as u128) << shift;
|
||||
}
|
||||
shift += 8;
|
||||
}
|
||||
|
||||
shift = 0;
|
||||
let mut hash2 = 0;
|
||||
for &ch in value.as_bytes().iter().skip(16).take(16) {
|
||||
hash2 |= (ch as u128) << shift;
|
||||
shift += 8;
|
||||
}
|
||||
|
||||
println!(
|
||||
"0x{} => {{}} // {}",
|
||||
format!("{hash:x}")
|
||||
.as_bytes()
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|s| std::str::from_utf8(s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_"),
|
||||
value
|
||||
);
|
||||
/*println!(
|
||||
"(0x{}, 0x{}) => Filter::{}(),",
|
||||
format!("{hash:x}")
|
||||
.as_bytes()
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|s| std::str::from_utf8(s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_"),
|
||||
format!("{hash2:x}")
|
||||
.as_bytes()
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|s| std::str::from_utf8(s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_"),
|
||||
value
|
||||
);*/
|
||||
|
||||
/*let mut hash = 0;
|
||||
let mut shift = 0;
|
||||
let mut first_ch = 0;
|
||||
let mut name = Vec::new();
|
||||
|
||||
for (pos, &ch) in value.as_bytes().iter().take(16).enumerate() {
|
||||
if pos == 0 {
|
||||
first_ch = ch.to_ascii_lowercase();
|
||||
name.push(ch.to_ascii_uppercase());
|
||||
} else {
|
||||
hash |= (ch as u128) << shift;
|
||||
shift += 8;
|
||||
name.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
//println!("Property::{} => {{}}", std::str::from_utf8(&name).unwrap());
|
||||
|
||||
table
|
||||
.entry(first_ch)
|
||||
.or_insert_with(|| vec![])
|
||||
.push((hash, name));*/
|
||||
}
|
||||
|
||||
/*for (k, v) in table {
|
||||
println!("b'{}' => match hash {{", k as char);
|
||||
for (hash, value) in v {
|
||||
println!(
|
||||
" 0x{} => Property::{},",
|
||||
format!("{hash:x}")
|
||||
.as_bytes()
|
||||
.chunks(4)
|
||||
.into_iter()
|
||||
.map(|s| std::str::from_utf8(s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_"),
|
||||
std::str::from_utf8(&value).unwrap()
|
||||
);
|
||||
}
|
||||
println!(" _ => parser.invalid_property()?,");
|
||||
println!("}}");
|
||||
}*/
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -2,7 +2,10 @@ use ahash::AHashMap;
|
|||
use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::{
|
||||
error::{method::MethodError, set::SetError},
|
||||
error::{
|
||||
method::MethodError,
|
||||
set::{InvalidProperty, SetError},
|
||||
},
|
||||
object::{email_submission, mailbox, sieve, Object},
|
||||
parser::{json::Parser, Error, JsonObjectParser, Token},
|
||||
request::{
|
||||
|
@ -342,3 +345,58 @@ impl RequestPropertyParser for RequestArguments {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SetRequest {
|
||||
pub fn validate(&self, max_objects_in_set: usize) -> Result<(), MethodError> {
|
||||
if self.create.as_ref().map_or(0, |objs| objs.len())
|
||||
+ self.update.as_ref().map_or(0, |objs| objs.len())
|
||||
+ self.destroy.as_ref().map_or(0, |objs| {
|
||||
if let MaybeReference::Value(ids) = objs {
|
||||
ids.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
> max_objects_in_set
|
||||
{
|
||||
Err(MethodError::RequestTooLarge)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_create(&mut self) -> VecMap<String, Object<SetValue>> {
|
||||
self.create.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn unwrap_update(&mut self) -> VecMap<Id, Object<SetValue>> {
|
||||
self.update.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn unwrap_destroy(&mut self) -> Vec<Id> {
|
||||
self.destroy
|
||||
.take()
|
||||
.map(|ids| ids.unwrap())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl SetResponse {
|
||||
pub fn invalid_property_create(&mut self, id: String, property: impl Into<InvalidProperty>) {
|
||||
self.not_created.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(property)
|
||||
.with_description("Invalid property or value.".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn invalid_property_update(&mut self, id: Id, property: impl Into<InvalidProperty>) {
|
||||
self.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(property)
|
||||
.with_description("Invalid property or value.".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
types::{
|
||||
id::Id,
|
||||
property::Property,
|
||||
value::{SetValue, Value},
|
||||
value::{MaybePatchValue, SetValue, Value},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -294,6 +294,53 @@ impl Response {
|
|||
}
|
||||
}
|
||||
|
||||
impl Object<SetValue> {
|
||||
pub fn iterate_and_eval_references(
|
||||
self,
|
||||
response: &Response,
|
||||
) -> impl Iterator<Item = Result<(Property, MaybePatchValue), MethodError>> + '_ {
|
||||
self.properties
|
||||
.into_iter()
|
||||
.map(|(property, set_value)| match set_value {
|
||||
SetValue::Value(value) => Ok((property, MaybePatchValue::Value(value))),
|
||||
SetValue::Patch(patch) => Ok((property, MaybePatchValue::Patch(patch))),
|
||||
SetValue::IdReference(MaybeReference::Reference(id_ref)) => {
|
||||
if let Some(id) = response.created_ids.get(&id_ref) {
|
||||
Ok((property, MaybePatchValue::Value(Value::Id(*id))))
|
||||
} else {
|
||||
Err(MethodError::InvalidResultReference(format!(
|
||||
"Id reference {id_ref:?} not found."
|
||||
)))
|
||||
}
|
||||
}
|
||||
SetValue::IdReference(MaybeReference::Value(id)) => {
|
||||
Ok((property, MaybePatchValue::Value(Value::Id(id))))
|
||||
}
|
||||
SetValue::IdReferences(id_refs) => {
|
||||
let mut ids = Vec::with_capacity(id_refs.len());
|
||||
for id_ref in id_refs {
|
||||
match id_ref {
|
||||
MaybeReference::Value(id) => {
|
||||
ids.push(Value::Id(id));
|
||||
}
|
||||
MaybeReference::Reference(id_ref) => {
|
||||
if let Some(id) = response.created_ids.get(&id_ref) {
|
||||
ids.push(Value::Id(*id));
|
||||
} else {
|
||||
return Err(MethodError::InvalidResultReference(format!(
|
||||
"Id reference {id_ref:?} not found."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((property, MaybePatchValue::Value(Value::List(ids))))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub fn unwrap_ids(self, rr: &ResultReference) -> Result<Vec<Id>, MethodError> {
|
||||
if let EvalResult::Values(values) = self {
|
||||
|
|
|
@ -29,7 +29,7 @@ use store::{
|
|||
BlobKind,
|
||||
};
|
||||
use utils::codec::{
|
||||
base32_custom::Base32Writer,
|
||||
base32_custom::{Base32Reader, Base32Writer},
|
||||
leb128::{Leb128Iterator, Leb128Writer},
|
||||
};
|
||||
|
||||
|
@ -109,6 +109,10 @@ impl BlobId {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_base32(value: &str) -> Option<Self> {
|
||||
BlobId::from_iter(&mut Base32Reader::new(value.as_bytes()))
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_iter<T, U>(it: &mut T) -> Option<Self>
|
||||
where
|
||||
|
|
|
@ -668,6 +668,23 @@ impl Property {
|
|||
Property::_T(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_rfc_header(&self) -> RfcHeader {
|
||||
match self {
|
||||
Property::MessageId => RfcHeader::MessageId,
|
||||
Property::InReplyTo => RfcHeader::InReplyTo,
|
||||
Property::References => RfcHeader::References,
|
||||
Property::Sender => RfcHeader::Sender,
|
||||
Property::From => RfcHeader::From,
|
||||
Property::To => RfcHeader::To,
|
||||
Property::Cc => RfcHeader::Cc,
|
||||
Property::Bcc => RfcHeader::Bcc,
|
||||
Property::ReplyTo => RfcHeader::ReplyTo,
|
||||
Property::Subject => RfcHeader::Subject,
|
||||
Property::SentAt => RfcHeader::Date,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Property {
|
||||
|
@ -968,6 +985,7 @@ impl From<RfcHeader> for Property {
|
|||
RfcHeader::InReplyTo => Property::InReplyTo,
|
||||
RfcHeader::MessageId => Property::MessageId,
|
||||
RfcHeader::References => Property::References,
|
||||
RfcHeader::ResentMessageId => Property::EmailIds,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,12 @@ pub enum SetValue {
|
|||
ResultReference(ResultReference),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MaybePatchValue {
|
||||
Value(Value),
|
||||
Patch(Vec<Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetValueMap<T> {
|
||||
pub values: Vec<T>,
|
||||
|
@ -161,7 +167,70 @@ impl Value {
|
|||
.unwrap_bool_or_null("")?
|
||||
.map(Value::Bool)
|
||||
.unwrap_or(Value::Null)),
|
||||
_ => Value::parse::<String, String>(parser.next_token()?, parser),
|
||||
_ => Value::parse::<ObjectProperty, String>(parser.next_token()?, parser),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_id(self) -> Id {
|
||||
match self {
|
||||
Value::Id(id) => id,
|
||||
_ => panic!("Expected id"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_bool(self) -> bool {
|
||||
match self {
|
||||
Value::Bool(b) => b,
|
||||
_ => panic!("Expected bool"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_keyword(self) -> Keyword {
|
||||
match self {
|
||||
Value::Keyword(k) => k,
|
||||
_ => panic!("Expected keyword"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_string(self) -> Option<String> {
|
||||
match self {
|
||||
Value::Text(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_object(self) -> Option<Object<Value>> {
|
||||
match self {
|
||||
Value::Object(o) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_list(self) -> Option<Vec<Value>> {
|
||||
match self {
|
||||
Value::List(l) => Some(l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_date(self) -> Option<UTCDate> {
|
||||
match self {
|
||||
Value::Date(d) => Some(d),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_blob_id(self) -> Option<BlobId> {
|
||||
match self {
|
||||
Value::BlobId(b) => Some(b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unwrap_uint(self) -> Option<u64> {
|
||||
match self {
|
||||
Value::UnsignedInt(u) => Some(u),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,3 +19,6 @@ http-body-util = "0.1.0-rc.2"
|
|||
form_urlencoded = "1.1.0"
|
||||
tracing = "0.1"
|
||||
tokio = { version = "1.23", features = ["rt"] }
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
|
|
|
@ -53,8 +53,7 @@ impl JMAP {
|
|||
("download", &Method::GET) => {
|
||||
if let (Some(account_id), Some(blob_id), Some(name)) = (
|
||||
path.next().and_then(|p| Id::from_bytes(p.as_bytes())),
|
||||
path.next()
|
||||
.and_then(|p| BlobId::from_iter(&mut p.as_bytes().iter())),
|
||||
path.next().and_then(BlobId::from_base32),
|
||||
path.next(),
|
||||
) {
|
||||
return match self.blob_download(&blob_id, account_id.document_id()).await {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use jmap_proto::{
|
||||
error::request::RequestError,
|
||||
error::{method::MethodError, request::RequestError},
|
||||
method::{get, query},
|
||||
request::{method::MethodName, Request, RequestMethod},
|
||||
request::{
|
||||
method::{MethodName, MethodObject},
|
||||
Request, RequestMethod,
|
||||
},
|
||||
response::{Response, ResponseMethod},
|
||||
};
|
||||
|
||||
|
@ -27,12 +30,12 @@ impl JMAP {
|
|||
}
|
||||
|
||||
let method_response: ResponseMethod = match call.method {
|
||||
RequestMethod::Get(mut call) => match call.take_arguments() {
|
||||
RequestMethod::Get(mut req) => match req.take_arguments() {
|
||||
get::RequestArguments::Email(arguments) => {
|
||||
self.email_get(call.with_arguments(arguments)).await.into()
|
||||
self.email_get(req.with_arguments(arguments)).await.into()
|
||||
}
|
||||
get::RequestArguments::Mailbox => todo!(),
|
||||
get::RequestArguments::Thread => self.thread_get(call).await.into(),
|
||||
get::RequestArguments::Thread => self.thread_get(req).await.into(),
|
||||
get::RequestArguments::Identity => todo!(),
|
||||
get::RequestArguments::EmailSubmission => todo!(),
|
||||
get::RequestArguments::PushSubscription => todo!(),
|
||||
|
@ -40,26 +43,28 @@ impl JMAP {
|
|||
get::RequestArguments::VacationResponse => todo!(),
|
||||
get::RequestArguments::Principal => todo!(),
|
||||
},
|
||||
RequestMethod::Query(mut call) => match call.take_arguments() {
|
||||
query::RequestArguments::Email(arguments) => self
|
||||
.email_query(call.with_arguments(arguments))
|
||||
.await
|
||||
.into(),
|
||||
RequestMethod::Query(mut req) => match req.take_arguments() {
|
||||
query::RequestArguments::Email(arguments) => {
|
||||
self.email_query(req.with_arguments(arguments)).await.into()
|
||||
}
|
||||
query::RequestArguments::Mailbox(_) => todo!(),
|
||||
query::RequestArguments::EmailSubmission => todo!(),
|
||||
query::RequestArguments::SieveScript => todo!(),
|
||||
query::RequestArguments::Principal => todo!(),
|
||||
},
|
||||
RequestMethod::Set(_) => todo!(),
|
||||
RequestMethod::Set(req) => match call.name.obj {
|
||||
MethodObject::Email => self.email_set(req, &response).await.into(),
|
||||
_ => MethodError::UnknownMethod(format!("{}/set", call.name.obj)).into(),
|
||||
},
|
||||
RequestMethod::Changes(_) => todo!(),
|
||||
RequestMethod::Copy(_) => todo!(),
|
||||
RequestMethod::CopyBlob(_) => todo!(),
|
||||
RequestMethod::ImportEmail(call) => self.email_import(call).await.into(),
|
||||
RequestMethod::ImportEmail(req) => self.email_import(req).await.into(),
|
||||
RequestMethod::ParseEmail(_) => todo!(),
|
||||
RequestMethod::QueryChanges(_) => todo!(),
|
||||
RequestMethod::SearchSnippet(_) => todo!(),
|
||||
RequestMethod::ValidateScript(_) => todo!(),
|
||||
RequestMethod::Echo(call) => call.into(),
|
||||
RequestMethod::Echo(req) => req.into(),
|
||||
RequestMethod::Error(error) => error.into(),
|
||||
};
|
||||
|
||||
|
|
|
@ -24,4 +24,20 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assert_state(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
if_in_state: &Option<State>,
|
||||
) -> Result<State, MethodError> {
|
||||
let old_state: State = self.get_state(account_id, collection).await?;
|
||||
if let Some(if_in_state) = if_in_state {
|
||||
if &old_state != if_in_state {
|
||||
return Err(MethodError::StateMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(old_state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use jmap_proto::{
|
||||
object::Object,
|
||||
types::{
|
||||
property::{HeaderForm, Property},
|
||||
property::{HeaderForm, HeaderProperty, Property},
|
||||
value::Value,
|
||||
},
|
||||
};
|
||||
use mail_builder::{
|
||||
headers::{
|
||||
address::{Address, EmailAddress, GroupedAddresses},
|
||||
date::Date,
|
||||
message_id::MessageId,
|
||||
raw::Raw,
|
||||
text::Text,
|
||||
url::URL,
|
||||
},
|
||||
MessageBuilder,
|
||||
};
|
||||
use mail_parser::{parsers::MessageStream, Addr, HeaderName, HeaderValue, MessagePart, RfcHeader};
|
||||
|
||||
pub trait IntoForm {
|
||||
|
@ -16,6 +29,16 @@ pub trait HeaderToValue {
|
|||
fn headers_to_value(&self, raw_message: &[u8]) -> Value;
|
||||
}
|
||||
|
||||
pub trait ValueToHeader<'x> {
|
||||
fn try_into_grouped_addresses(self) -> Option<GroupedAddresses<'x>>;
|
||||
fn try_into_address_list(self) -> Option<Vec<Address<'x>>>;
|
||||
fn try_into_address(self) -> Option<EmailAddress<'x>>;
|
||||
}
|
||||
|
||||
pub trait BuildHeader: Sized {
|
||||
fn build_header(self, header: HeaderProperty, value: Value) -> Result<Self, HeaderProperty>;
|
||||
}
|
||||
|
||||
impl HeaderToValue for MessagePart<'_> {
|
||||
fn header_to_value(&self, property: &Property, raw_message: &[u8]) -> Value {
|
||||
let (header_name, form, all) = match property {
|
||||
|
@ -209,6 +232,165 @@ impl IntoForm for HeaderValue<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'x> ValueToHeader<'x> for Value {
|
||||
fn try_into_grouped_addresses(self) -> Option<GroupedAddresses<'x>> {
|
||||
let mut obj = self.try_unwrap_object()?;
|
||||
Some(GroupedAddresses {
|
||||
name: obj
|
||||
.properties
|
||||
.remove(&Property::Name)
|
||||
.and_then(|n| n.try_unwrap_string())
|
||||
.map(|n| n.into()),
|
||||
addresses: obj
|
||||
.properties
|
||||
.remove(&Property::Addresses)?
|
||||
.try_into_address_list()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_into_address_list(self) -> Option<Vec<Address<'x>>> {
|
||||
let list = self.try_unwrap_list()?;
|
||||
let mut addresses = Vec::with_capacity(list.len());
|
||||
for value in list {
|
||||
addresses.push(Address::Address(value.try_into_address()?));
|
||||
}
|
||||
Some(addresses)
|
||||
}
|
||||
|
||||
fn try_into_address(self) -> Option<EmailAddress<'x>> {
|
||||
let mut obj = self.try_unwrap_object()?;
|
||||
Some(EmailAddress {
|
||||
name: obj
|
||||
.properties
|
||||
.remove(&Property::Name)
|
||||
.and_then(|n| n.try_unwrap_string())
|
||||
.map(|n| n.into()),
|
||||
email: obj
|
||||
.properties
|
||||
.remove(&Property::Email)?
|
||||
.try_unwrap_string()?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildHeader for MessageBuilder<'_> {
|
||||
fn build_header(self, header: HeaderProperty, value: Value) -> Result<Self, HeaderProperty> {
|
||||
Ok(match (&header.form, header.all, value) {
|
||||
(HeaderForm::Raw, false, Value::Text(value)) => {
|
||||
self.header(header.header, Raw::from(value))
|
||||
}
|
||||
(HeaderForm::Raw, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value
|
||||
.into_iter()
|
||||
.filter_map(|v| Raw::from(v.try_unwrap_string()?).into()),
|
||||
),
|
||||
(HeaderForm::Date, false, Value::Date(value)) => {
|
||||
self.header(header.header, Date::new(value.timestamp()))
|
||||
}
|
||||
(HeaderForm::Date, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value
|
||||
.into_iter()
|
||||
.filter_map(|v| Date::new(v.try_unwrap_date()?.timestamp()).into()),
|
||||
),
|
||||
(HeaderForm::Text, false, Value::Text(value)) => {
|
||||
self.header(header.header, Text::from(value))
|
||||
}
|
||||
(HeaderForm::Text, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value
|
||||
.into_iter()
|
||||
.filter_map(|v| Text::from(v.try_unwrap_string()?).into()),
|
||||
),
|
||||
(HeaderForm::URLs, false, Value::List(value)) => self.header(
|
||||
header.header,
|
||||
URL {
|
||||
url: value
|
||||
.into_iter()
|
||||
.filter_map(|v| Cow::from(v.try_unwrap_string()?).into())
|
||||
.collect(),
|
||||
},
|
||||
),
|
||||
(HeaderForm::URLs, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value.into_iter().filter_map(|value| {
|
||||
URL {
|
||||
url: value
|
||||
.try_unwrap_list()?
|
||||
.into_iter()
|
||||
.filter_map(|v| Cow::from(v.try_unwrap_string()?).into())
|
||||
.collect(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
(HeaderForm::MessageIds, false, Value::List(value)) => self.header(
|
||||
header.header,
|
||||
MessageId {
|
||||
id: value
|
||||
.into_iter()
|
||||
.filter_map(|v| Cow::from(v.try_unwrap_string()?).into())
|
||||
.collect(),
|
||||
},
|
||||
),
|
||||
(HeaderForm::MessageIds, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value.into_iter().filter_map(|value| {
|
||||
MessageId {
|
||||
id: value
|
||||
.try_unwrap_list()?
|
||||
.into_iter()
|
||||
.filter_map(|v| Cow::from(v.try_unwrap_string()?).into())
|
||||
.collect(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
(HeaderForm::Addresses, false, Value::List(value)) => self.header(
|
||||
header.header,
|
||||
Address::new_list(
|
||||
value
|
||||
.into_iter()
|
||||
.filter_map(|v| Address::Address(v.try_into_address()?).into())
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
(HeaderForm::Addresses, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value
|
||||
.into_iter()
|
||||
.filter_map(|v| Address::new_list(v.try_into_address_list()?).into()),
|
||||
),
|
||||
(HeaderForm::GroupedAddresses, false, Value::List(value)) => self.header(
|
||||
header.header,
|
||||
Address::new_list(
|
||||
value
|
||||
.into_iter()
|
||||
.filter_map(|v| Address::Group(v.try_into_grouped_addresses()?).into())
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
(HeaderForm::GroupedAddresses, true, Value::List(value)) => self.headers(
|
||||
header.header,
|
||||
value.into_iter().filter_map(|v| {
|
||||
Address::new_list(
|
||||
v.try_unwrap_list()?
|
||||
.into_iter()
|
||||
.filter_map(|v| Address::Group(v.try_into_grouped_addresses()?).into())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
_ => {
|
||||
return Err(header);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
trait ByteTrim {
|
||||
fn trim_end(&self) -> Self;
|
||||
}
|
||||
|
|
|
@ -17,12 +17,9 @@ impl JMAP {
|
|||
) -> Result<ImportEmailResponse, MethodError> {
|
||||
// Validate state
|
||||
let account_id = request.account_id.document_id();
|
||||
let old_state: State = self.get_state(account_id, Collection::Email).await?;
|
||||
if let Some(if_in_state) = request.if_in_state {
|
||||
if old_state != if_in_state {
|
||||
return Err(MethodError::StateMismatch);
|
||||
}
|
||||
}
|
||||
let old_state: State = self
|
||||
.assert_state(account_id, Collection::Email, &request.if_in_state)
|
||||
.await?;
|
||||
|
||||
let cococ = "implement ACLS";
|
||||
let valid_mailbox_ids = self
|
||||
|
|
|
@ -30,6 +30,11 @@ pub const MAX_SORT_FIELD_LENGTH: usize = 255;
|
|||
pub const MAX_STORED_FIELD_LENGTH: usize = 512;
|
||||
pub const PREVIEW_LENGTH: usize = 256;
|
||||
|
||||
pub struct SortedAddressBuilder {
|
||||
last_is_space: bool,
|
||||
buf: String,
|
||||
}
|
||||
|
||||
pub(super) trait IndexMessage {
|
||||
fn index_message(
|
||||
&mut self,
|
||||
|
@ -50,7 +55,7 @@ impl IndexMessage for BatchBuilder {
|
|||
received_at: u64,
|
||||
default_language: Language,
|
||||
) -> store::Result<()> {
|
||||
let mut object = Object::with_capacity(15);
|
||||
let mut metadata = Object::with_capacity(15);
|
||||
|
||||
// Index keywords
|
||||
self.value(Property::Keywords, keywords, F_VALUE | F_BITMAP);
|
||||
|
@ -59,11 +64,11 @@ impl IndexMessage for BatchBuilder {
|
|||
self.value(Property::MailboxIds, mailbox_ids, F_VALUE | F_BITMAP);
|
||||
|
||||
// Index size
|
||||
object.append(Property::Size, message.raw_message.len());
|
||||
metadata.append(Property::Size, message.raw_message.len());
|
||||
self.value(Property::Size, message.raw_message.len() as u32, F_INDEX);
|
||||
|
||||
// Index receivedAt
|
||||
object.append(
|
||||
metadata.append(
|
||||
Property::ReceivedAt,
|
||||
Value::Date(UTCDate::from_timestamp(received_at as i64)),
|
||||
);
|
||||
|
@ -89,6 +94,7 @@ impl IndexMessage for BatchBuilder {
|
|||
let part_language = part.language().unwrap_or(language);
|
||||
if part_id == 0 {
|
||||
language = part_language;
|
||||
let mut extra_ids = Vec::new();
|
||||
for header in part.headers.into_iter().rev() {
|
||||
if let HeaderName::Rfc(rfc_header) = header.name {
|
||||
// Index hasHeader property
|
||||
|
@ -103,7 +109,6 @@ impl IndexMessage for BatchBuilder {
|
|||
header.value.visit_text(|id| {
|
||||
// Add ids to inverted index
|
||||
if id.len() < MAX_ID_LENGTH {
|
||||
println!("indexing {}: {}", rfc_header.as_str(), id);
|
||||
self.value(Property::MessageId, id, F_INDEX);
|
||||
}
|
||||
|
||||
|
@ -123,7 +128,7 @@ impl IndexMessage for BatchBuilder {
|
|||
| RfcHeader::References
|
||||
) && !seen_headers[rfc_header as usize]
|
||||
{
|
||||
object.append(
|
||||
metadata.append(
|
||||
rfc_header.into(),
|
||||
header
|
||||
.value
|
||||
|
@ -131,6 +136,10 @@ impl IndexMessage for BatchBuilder {
|
|||
.into_form(&HeaderForm::MessageIds),
|
||||
);
|
||||
seen_headers[rfc_header as usize] = true;
|
||||
} else {
|
||||
header.value.into_visit_text(|id| {
|
||||
extra_ids.push(Value::Text(id));
|
||||
});
|
||||
}
|
||||
}
|
||||
RfcHeader::From
|
||||
|
@ -148,31 +157,20 @@ impl IndexMessage for BatchBuilder {
|
|||
| RfcHeader::Cc
|
||||
| RfcHeader::Bcc
|
||||
) {
|
||||
let mut sort_text =
|
||||
String::with_capacity(MAX_SORT_FIELD_LENGTH);
|
||||
let mut sort_text = SortedAddressBuilder::new();
|
||||
let mut found_addr = seen_header;
|
||||
let mut last_is_space = true;
|
||||
|
||||
header.value.visit_addresses(|value, is_addr| {
|
||||
header.value.visit_addresses(|element, value| {
|
||||
if !found_addr {
|
||||
if !sort_text.is_empty() {
|
||||
sort_text.push(' ');
|
||||
last_is_space = true;
|
||||
}
|
||||
found_addr = is_addr;
|
||||
'outer: for ch in value.chars() {
|
||||
for ch in ch.to_lowercase() {
|
||||
if sort_text.len() < MAX_SORT_FIELD_LENGTH {
|
||||
let is_space = ch.is_whitespace();
|
||||
if !is_space || !last_is_space {
|
||||
sort_text.push(ch);
|
||||
last_is_space = is_space;
|
||||
}
|
||||
} else {
|
||||
found_addr = true;
|
||||
break 'outer;
|
||||
}
|
||||
match element {
|
||||
AddressElement::Name => {
|
||||
found_addr = sort_text.push(value);
|
||||
}
|
||||
AddressElement::Address => {
|
||||
sort_text.push(value);
|
||||
found_addr = true;
|
||||
}
|
||||
AddressElement::GroupName => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,21 +180,13 @@ impl IndexMessage for BatchBuilder {
|
|||
|
||||
if !seen_header {
|
||||
// Add address to inverted index
|
||||
self.value(
|
||||
u8::from(&property),
|
||||
if !sort_text.is_empty() {
|
||||
&sort_text
|
||||
} else {
|
||||
"!"
|
||||
},
|
||||
F_INDEX,
|
||||
);
|
||||
self.value(u8::from(&property), sort_text.build(), F_INDEX);
|
||||
}
|
||||
}
|
||||
|
||||
if !seen_header {
|
||||
// Add address to object
|
||||
object.append(
|
||||
// Add address to metadata
|
||||
metadata.append(
|
||||
property,
|
||||
header
|
||||
.value
|
||||
|
@ -215,7 +205,7 @@ impl IndexMessage for BatchBuilder {
|
|||
F_INDEX,
|
||||
);
|
||||
}
|
||||
object.append(
|
||||
metadata.append(
|
||||
Property::SentAt,
|
||||
header.value.into_form(&HeaderForm::Date),
|
||||
);
|
||||
|
@ -233,8 +223,8 @@ impl IndexMessage for BatchBuilder {
|
|||
};
|
||||
|
||||
if !seen_headers[rfc_header as usize] {
|
||||
// Add to object
|
||||
object.append(
|
||||
// Add to metadata
|
||||
metadata.append(
|
||||
Property::Subject,
|
||||
header
|
||||
.value
|
||||
|
@ -278,14 +268,19 @@ impl IndexMessage for BatchBuilder {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any extra Ids to metadata
|
||||
if !extra_ids.is_empty() {
|
||||
metadata.append(Property::EmailIds, Value::List(extra_ids));
|
||||
}
|
||||
}
|
||||
|
||||
match part.body {
|
||||
PartType::Text(text) => {
|
||||
if part_id == preview_part_id {
|
||||
object.append(
|
||||
metadata.append(
|
||||
Property::Preview,
|
||||
preview_text(text.clone(), PREVIEW_LENGTH),
|
||||
preview_text(text.replace('\r', "").into(), PREVIEW_LENGTH),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -300,9 +295,9 @@ impl IndexMessage for BatchBuilder {
|
|||
PartType::Html(html) => {
|
||||
let text = html_to_text(&html);
|
||||
if part_id == preview_part_id {
|
||||
object.append(
|
||||
metadata.append(
|
||||
Property::Preview,
|
||||
preview_text(text.clone().into(), PREVIEW_LENGTH),
|
||||
preview_text(text.replace('\r', "").into(), PREVIEW_LENGTH),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -354,21 +349,67 @@ impl IndexMessage for BatchBuilder {
|
|||
}
|
||||
|
||||
// Store and index hasAttachment property
|
||||
object.append(Property::HasAttachment, has_attachments);
|
||||
metadata.append(Property::HasAttachment, has_attachments);
|
||||
if has_attachments {
|
||||
self.bitmap(Property::HasAttachment, (), 0);
|
||||
}
|
||||
|
||||
// Store properties
|
||||
self.value(Property::BodyStructure, object, F_VALUE);
|
||||
self.value(Property::BodyStructure, metadata, F_VALUE);
|
||||
|
||||
// Store full text index
|
||||
self.custom(fts)?;
|
||||
self.custom(fts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SortedAddressBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_is_space: true,
|
||||
buf: String::with_capacity(32),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, text: &str) -> bool {
|
||||
if !text.is_empty() {
|
||||
if !self.buf.is_empty() {
|
||||
self.buf.push(' ');
|
||||
self.last_is_space = true;
|
||||
}
|
||||
for ch in text.chars() {
|
||||
for ch in ch.to_lowercase() {
|
||||
if self.buf.len() < MAX_SORT_FIELD_LENGTH {
|
||||
let is_space = ch.is_whitespace();
|
||||
if !is_space || !self.last_is_space {
|
||||
self.buf.push(ch);
|
||||
self.last_is_space = is_space;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
if !self.buf.is_empty() {
|
||||
self.buf
|
||||
} else {
|
||||
"!".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SortedAddressBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
trait GetContentLanguage {
|
||||
fn language(&self) -> Option<Language>;
|
||||
}
|
||||
|
@ -390,55 +431,64 @@ impl GetContentLanguage for MessagePart<'_> {
|
|||
}
|
||||
|
||||
trait VisitValues {
|
||||
fn visit_addresses(&self, visitor: impl FnMut(&str, bool));
|
||||
fn visit_addresses(&self, visitor: impl FnMut(AddressElement, &str));
|
||||
fn visit_text(&self, visitor: impl FnMut(&str));
|
||||
fn into_visit_text(self, visitor: impl FnMut(String));
|
||||
}
|
||||
|
||||
enum AddressElement {
|
||||
Name,
|
||||
Address,
|
||||
GroupName,
|
||||
}
|
||||
|
||||
impl VisitValues for HeaderValue<'_> {
|
||||
fn visit_addresses(&self, mut visitor: impl FnMut(&str, bool)) {
|
||||
fn visit_addresses(&self, mut visitor: impl FnMut(AddressElement, &str)) {
|
||||
match self {
|
||||
HeaderValue::Address(addr) => {
|
||||
if let Some(name) = &addr.name {
|
||||
visitor(name.as_ref(), false);
|
||||
visitor(AddressElement::Name, name);
|
||||
}
|
||||
if let Some(addr) = &addr.address {
|
||||
visitor(addr.as_ref(), true);
|
||||
visitor(AddressElement::Address, addr);
|
||||
}
|
||||
}
|
||||
HeaderValue::AddressList(addr_list) => {
|
||||
for addr in addr_list {
|
||||
if let Some(name) = &addr.name {
|
||||
visitor(name.as_ref(), false);
|
||||
visitor(AddressElement::Name, name);
|
||||
}
|
||||
if let Some(addr) = &addr.address {
|
||||
visitor(addr.as_ref(), true);
|
||||
visitor(AddressElement::Address, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
HeaderValue::Group(group) => {
|
||||
if let Some(name) = &group.name {
|
||||
visitor(name.as_ref(), false);
|
||||
visitor(AddressElement::GroupName, name);
|
||||
}
|
||||
|
||||
for addr in &group.addresses {
|
||||
if let Some(name) = &addr.name {
|
||||
visitor(name.as_ref(), false);
|
||||
visitor(AddressElement::Name, name);
|
||||
}
|
||||
if let Some(addr) = &addr.address {
|
||||
visitor(addr.as_ref(), true);
|
||||
visitor(AddressElement::Address, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
HeaderValue::GroupList(groups) => {
|
||||
for group in groups {
|
||||
if let Some(name) = &group.name {
|
||||
visitor(name.as_ref(), false);
|
||||
visitor(AddressElement::GroupName, name);
|
||||
}
|
||||
|
||||
for addr in &group.addresses {
|
||||
if let Some(name) = &addr.name {
|
||||
visitor(name.as_ref(), false);
|
||||
visitor(AddressElement::Name, name);
|
||||
}
|
||||
if let Some(addr) = &addr.address {
|
||||
visitor(addr.as_ref(), true);
|
||||
visitor(AddressElement::Address, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -446,6 +496,7 @@ impl VisitValues for HeaderValue<'_> {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_text(&self, mut visitor: impl FnMut(&str)) {
|
||||
match &self {
|
||||
HeaderValue::Text(text) => {
|
||||
|
@ -459,6 +510,20 @@ impl VisitValues for HeaderValue<'_> {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_visit_text(self, mut visitor: impl FnMut(String)) {
|
||||
match self {
|
||||
HeaderValue::Text(text) => {
|
||||
visitor(text.into_owned());
|
||||
}
|
||||
HeaderValue::TextList(texts) => {
|
||||
for text in texts {
|
||||
visitor(text.into_owned());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TrimTextValue {
|
||||
|
|
|
@ -104,7 +104,7 @@ impl JMAP {
|
|||
})?;
|
||||
let change_id = self
|
||||
.store
|
||||
.assign_change_id(account_id, Collection::Email)
|
||||
.assign_change_id(account_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
|
@ -178,14 +178,7 @@ impl JMAP {
|
|||
MaybeError::Temporary
|
||||
})?;
|
||||
batch.value(Property::ThreadId, thread_id, F_VALUE | F_BITMAP);
|
||||
batch.custom(changes).map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "email_ingest",
|
||||
error = ?err,
|
||||
"Failed to add changelog to write batch.");
|
||||
MaybeError::Temporary
|
||||
})?;
|
||||
batch.custom(changes);
|
||||
self.store.write(batch.build()).await.map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
|
@ -211,7 +204,6 @@ impl JMAP {
|
|||
) -> Result<Option<u32>, MaybeError> {
|
||||
let mut try_count = 0;
|
||||
|
||||
println!("-----------\nthread name: {:?}", thread_name);
|
||||
loop {
|
||||
// Find messages with matching references
|
||||
let mut filters = Vec::with_capacity(references.len() + 3);
|
||||
|
@ -235,8 +227,6 @@ impl JMAP {
|
|||
})?
|
||||
.results;
|
||||
|
||||
println!("found messages {:?}", results);
|
||||
|
||||
if results.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@ -266,7 +256,7 @@ impl JMAP {
|
|||
"Failed to obtain threadIds.");
|
||||
MaybeError::Temporary
|
||||
})?;
|
||||
println!("found thread ids {:?}", thread_ids);
|
||||
|
||||
if thread_ids.len() == 1 {
|
||||
return Ok(thread_ids.into_iter().next().unwrap());
|
||||
}
|
||||
|
@ -283,7 +273,7 @@ impl JMAP {
|
|||
thread_id = *thread_id_;
|
||||
}
|
||||
}
|
||||
println!("common thread id {:?}", thread_id);
|
||||
|
||||
if thread_id == u32::MAX {
|
||||
return Ok(None); // This should never happen
|
||||
} else if thread_counts.len() == 1 {
|
||||
|
@ -294,7 +284,7 @@ impl JMAP {
|
|||
let mut batch = BatchBuilder::new();
|
||||
let change_id = self
|
||||
.store
|
||||
.assign_change_id(account_id, Collection::Thread)
|
||||
.assign_change_id(account_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
|
@ -351,14 +341,7 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
}
|
||||
batch.custom(changes).map_err(|err| {
|
||||
tracing::error!(
|
||||
event = "error",
|
||||
context = "find_or_merge_thread",
|
||||
error = ?err,
|
||||
"Failed to add changelog to write batch.");
|
||||
MaybeError::Temporary
|
||||
})?;
|
||||
batch.custom(changes);
|
||||
|
||||
match self.store.write(batch.build()).await {
|
||||
Ok(_) => return Ok(Some(thread_id)),
|
||||
|
|
|
@ -5,3 +5,4 @@ pub mod import;
|
|||
pub mod index;
|
||||
pub mod ingest;
|
||||
pub mod query;
|
||||
pub mod set;
|
||||
|
|
1164
crates/jmap/src/email/set.rs
Normal file
1164
crates/jmap/src/email/set.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,13 +1,15 @@
|
|||
use api::session::BaseCapabilities;
|
||||
use jmap_proto::{
|
||||
error::method::MethodError,
|
||||
method::set::{SetRequest, SetResponse},
|
||||
request::reference::MaybeReference,
|
||||
types::{collection::Collection, property::Property},
|
||||
};
|
||||
use store::{
|
||||
fts::Language, roaring::RoaringBitmap, write::BitmapFamily, BitmapKey, Deserialize, Serialize,
|
||||
Store, ValueKey,
|
||||
ahash::AHashMap, fts::Language, roaring::RoaringBitmap, write::BitmapFamily, BitmapKey,
|
||||
Deserialize, Serialize, Store, ValueKey,
|
||||
};
|
||||
use utils::UnwrapFailure;
|
||||
use utils::{map::vec_map::VecMap, UnwrapFailure};
|
||||
|
||||
pub mod api;
|
||||
pub mod blob;
|
||||
|
@ -176,4 +178,42 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn prepare_set_response(
|
||||
&self,
|
||||
request: &SetRequest,
|
||||
collection: Collection,
|
||||
) -> Result<SetResponse, MethodError> {
|
||||
let n_create = request.create.as_ref().map_or(0, |objs| objs.len());
|
||||
let n_update = request.update.as_ref().map_or(0, |objs| objs.len());
|
||||
let n_destroy = request.destroy.as_ref().map_or(0, |objs| {
|
||||
if let MaybeReference::Value(ids) = objs {
|
||||
ids.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
if n_create + n_update + n_destroy > self.config.set_max_objects {
|
||||
return Err(MethodError::RequestTooLarge);
|
||||
}
|
||||
let old_state = self
|
||||
.assert_state(
|
||||
request.account_id.document_id(),
|
||||
collection,
|
||||
&request.if_in_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(SetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
new_state: old_state.clone().into(),
|
||||
old_state: old_state.into(),
|
||||
created: AHashMap::with_capacity(n_create),
|
||||
updated: VecMap::with_capacity(n_update),
|
||||
destroyed: Vec::with_capacity(n_destroy),
|
||||
not_created: VecMap::new(),
|
||||
not_updated: VecMap::new(),
|
||||
not_destroyed: VecMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,14 +242,7 @@ impl Store {
|
|||
}
|
||||
} else {
|
||||
// Find the next available id
|
||||
let mut key = BitmapKey {
|
||||
account_id,
|
||||
collection,
|
||||
family: BM_DOCUMENT_IDS,
|
||||
field: u8::MAX,
|
||||
key: b"",
|
||||
block_num: 0,
|
||||
};
|
||||
let mut key = BitmapKey::document_ids(account_id, collection);
|
||||
let begin = key.serialize();
|
||||
key.block_num = u32::MAX;
|
||||
let end = key.serialize();
|
||||
|
|
|
@ -108,12 +108,9 @@ impl Store {
|
|||
unreachable!()
|
||||
}
|
||||
|
||||
pub async fn assign_change_id(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: impl Into<u8>,
|
||||
) -> crate::Result<u64> {
|
||||
let key = IdCacheKey::new(account_id, collection.into());
|
||||
pub async fn assign_change_id(&self, account_id: u32) -> crate::Result<u64> {
|
||||
let collection = u8::MAX;
|
||||
let key = IdCacheKey::new(account_id, collection);
|
||||
for _ in 0..2 {
|
||||
if let Some(assigner) = self.id_assigner.lock().get_mut(&key) {
|
||||
return Ok(assigner.assign_change_id());
|
||||
|
|
|
@ -72,7 +72,7 @@ impl<'x> FtsIndexBuilder<'x> {
|
|||
}
|
||||
|
||||
impl<'x> IntoOperations for FtsIndexBuilder<'x> {
|
||||
fn build(self, batch: &mut BatchBuilder) -> crate::Result<()> {
|
||||
fn build(self, batch: &mut BatchBuilder) {
|
||||
let default_language = self
|
||||
.detect
|
||||
.most_frequent_language()
|
||||
|
@ -122,7 +122,5 @@ impl<'x> IntoOperations for FtsIndexBuilder<'x> {
|
|||
.ops
|
||||
.push(Operation::hash(&token, HASH_EXACT, field, true));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
65
crates/store/src/write/assert.rs
Normal file
65
crates/store/src/write/assert.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::Deserialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HashedValue<T: Deserialize> {
|
||||
pub hash: u64,
|
||||
pub inner: T,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AssertValue {
|
||||
U32(u32),
|
||||
U64(u64),
|
||||
Hash(u64),
|
||||
}
|
||||
|
||||
pub trait ToAssertValue {
|
||||
fn to_assert_value(&self) -> AssertValue;
|
||||
}
|
||||
|
||||
impl ToAssertValue for u64 {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::U64(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for u32 {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::U32(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deserialize> ToAssertValue for HashedValue<T> {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::Hash(self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deserialize> ToAssertValue for &HashedValue<T> {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::Hash(self.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssertValue {
|
||||
pub fn matches(&self, bytes: &[u8]) -> bool {
|
||||
match self {
|
||||
AssertValue::U32(v) => {
|
||||
bytes.len() == std::mem::size_of::<u32>() && u32::deserialize(bytes).unwrap() == *v
|
||||
}
|
||||
AssertValue::U64(v) => {
|
||||
bytes.len() == std::mem::size_of::<u64>() && u64::deserialize(bytes).unwrap() == *v
|
||||
}
|
||||
AssertValue::Hash(v) => xxhash_rust::xxh3::xxh3_64(bytes) == *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deserialize> Deserialize for HashedValue<T> {
|
||||
fn deserialize(bytes: &[u8]) -> crate::Result<Self> {
|
||||
Ok(HashedValue {
|
||||
hash: xxhash_rust::xxh3::xxh3_64(bytes),
|
||||
inner: T::deserialize(bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
use crate::BM_DOCUMENT_IDS;
|
||||
|
||||
use super::{
|
||||
Batch, BatchBuilder, BitmapFamily, HasFlag, IntoOperations, Operation, Serialize,
|
||||
ToAssertValue, ToBitmaps, F_BITMAP, F_CLEAR, F_INDEX, F_VALUE,
|
||||
assert::ToAssertValue, Batch, BatchBuilder, BitmapFamily, HasFlag, IntoOperations, Operation,
|
||||
Serialize, ToBitmaps, F_BITMAP, F_CLEAR, F_INDEX, F_VALUE,
|
||||
};
|
||||
|
||||
impl BatchBuilder {
|
||||
|
@ -124,7 +124,7 @@ impl BatchBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn custom(&mut self, value: impl IntoOperations) -> crate::Result<()> {
|
||||
pub fn custom(&mut self, value: impl IntoOperations) {
|
||||
value.build(self)
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ impl ChangeLogBuilder {
|
|||
}
|
||||
|
||||
impl IntoOperations for ChangeLogBuilder {
|
||||
fn build(self, batch: &mut super::BatchBuilder) -> crate::Result<()> {
|
||||
fn build(self, batch: &mut super::BatchBuilder) {
|
||||
for (collection, changes) in self.changes {
|
||||
batch.ops.push(Operation::Log {
|
||||
change_id: self.change_id,
|
||||
|
@ -76,8 +76,6 @@ impl IntoOperations for ChangeLogBuilder {
|
|||
set: changes.serialize(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ use crate::{
|
|||
Deserialize, Serialize, BM_TAG, HASH_EXACT, TAG_ID, TAG_STATIC,
|
||||
};
|
||||
|
||||
use self::assert::AssertValue;
|
||||
|
||||
pub mod assert;
|
||||
pub mod batch;
|
||||
pub mod key;
|
||||
pub mod log;
|
||||
|
@ -68,13 +71,6 @@ pub enum Operation {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AssertValue {
|
||||
U32(u32),
|
||||
U64(u64),
|
||||
Hash(u64),
|
||||
}
|
||||
|
||||
impl Serialize for u32 {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
self.to_be_bytes().to_vec()
|
||||
|
@ -311,57 +307,14 @@ impl Serialize for () {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToBitmaps for () {
|
||||
fn to_bitmaps(&self, _ops: &mut Vec<Operation>, _field: u8, _set: bool) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoOperations {
|
||||
fn build(self, batch: &mut BatchBuilder) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
pub trait ToAssertValue {
|
||||
fn to_assert_value(&self) -> AssertValue;
|
||||
}
|
||||
|
||||
impl ToAssertValue for u64 {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::U64(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for u32 {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::U32(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for &[u8] {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
AssertValue::Hash(xxhash_rust::xxh3::xxh3_64(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAssertValue for Vec<u8> {
|
||||
fn to_assert_value(&self) -> AssertValue {
|
||||
self.as_slice().to_assert_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl AssertValue {
|
||||
pub fn matches(&self, bytes: &[u8]) -> bool {
|
||||
match self {
|
||||
AssertValue::U32(v) => {
|
||||
let coco = "fd";
|
||||
let a = u32::deserialize(bytes).unwrap();
|
||||
let b = *v;
|
||||
if a != b {
|
||||
println!("has {} != expected {}", a, b);
|
||||
}
|
||||
a == b
|
||||
//bytes.len() == std::mem::size_of::<u32>() && u32::deserialize(bytes).unwrap() == *v
|
||||
}
|
||||
AssertValue::U64(v) => {
|
||||
bytes.len() == std::mem::size_of::<u64>() && u64::deserialize(bytes).unwrap() == *v
|
||||
}
|
||||
AssertValue::Hash(v) => xxhash_rust::xxh3::xxh3_64(bytes) == *v,
|
||||
}
|
||||
}
|
||||
fn build(self, batch: &mut BatchBuilder);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
|
|
@ -6,10 +6,11 @@ resolver = "2"
|
|||
|
||||
[dependencies]
|
||||
store = { path = "../crates/store", features = ["test_mode"] }
|
||||
jmap = { path = "../crates/jmap" }
|
||||
jmap = { path = "../crates/jmap", features = ["test_mode"] }
|
||||
jmap_proto = { path = "../crates/jmap-proto" }
|
||||
utils = { path = "../crates/utils" }
|
||||
jmap-client = { git = "https://github.com/stalwartlabs/jmap-client", features = ["websockets", "debug", "async"] }
|
||||
#jmap-client = { git = "https://github.com/stalwartlabs/jmap-client", features = ["websockets", "debug", "async"] }
|
||||
jmap-client = { path = "/home/vagrant/code/jmap-client", features = ["websockets", "debug", "async"] }
|
||||
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
|
||||
tokio = { version = "1.23", features = ["full"] }
|
||||
csv = "1.1"
|
||||
|
|
341
tests/src/jmap/email_set.rs
Normal file
341
tests/src/jmap/email_set.rs
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2022, Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart JMAP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use jmap::JMAP;
|
||||
use jmap_client::{
|
||||
client::Client,
|
||||
core::set::{SetError, SetErrorType},
|
||||
email::{self, Email},
|
||||
mailbox::Role,
|
||||
Error, Set,
|
||||
};
|
||||
use jmap_proto::types::id::Id;
|
||||
|
||||
use super::{find_values, replace_blob_ids, replace_boundaries, replace_values};
|
||||
|
||||
pub async fn test(server: Arc<JMAP>, client: &mut Client) {
|
||||
println!("Running Email Set tests...");
|
||||
|
||||
let mailbox_id = "a";
|
||||
let coco = "fix";
|
||||
/*client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("JMAP Set", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();*/
|
||||
|
||||
create(client, &mailbox_id).await;
|
||||
//update(client, &mailbox_id).await;
|
||||
|
||||
let coco = "fd";
|
||||
//client.mailbox_destroy(&mailbox_id, true).await.unwrap();
|
||||
|
||||
//server.store.assert_is_empty();
|
||||
}
|
||||
|
||||
async fn create(client: &mut Client, mailbox_id: &str) {
|
||||
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_dir.push("resources");
|
||||
test_dir.push("jmap_mail_set");
|
||||
|
||||
for file_name in fs::read_dir(&test_dir).unwrap() {
|
||||
let mut file_name = file_name.as_ref().unwrap().path();
|
||||
if file_name.extension().map_or(true, |e| e != "json") {
|
||||
continue;
|
||||
}
|
||||
println!("Creating email from {:?}", file_name);
|
||||
|
||||
// Upload blobs
|
||||
let mut json_request = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
|
||||
let blob_values = find_values(&json_request, "\"blobId\"");
|
||||
if !blob_values.is_empty() {
|
||||
let mut blob_ids = Vec::with_capacity(blob_values.len());
|
||||
for blob_value in &blob_values {
|
||||
let blob_value = blob_value.replace("\\r", "\r").replace("\\n", "\n");
|
||||
blob_ids.push(
|
||||
client
|
||||
.upload(None, blob_value.into_bytes(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_blob_id(),
|
||||
);
|
||||
}
|
||||
json_request = replace_values(json_request, &blob_values, &blob_ids);
|
||||
}
|
||||
|
||||
// Create message and obtain its blobId
|
||||
let mut request = client.build();
|
||||
let mut create_item =
|
||||
serde_json::from_slice::<Email<Set>>(json_request.as_bytes()).unwrap();
|
||||
create_item.mailbox_ids([mailbox_id]);
|
||||
let create_id = request.set_email().create_item(create_item);
|
||||
let created_email = request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.created(&create_id)
|
||||
.unwrap();
|
||||
|
||||
// Download raw message
|
||||
let raw_message = client
|
||||
.download(created_email.blob_id().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Fetch message
|
||||
let mut request = client.build();
|
||||
request
|
||||
.get_email()
|
||||
.ids([created_email.id().unwrap()])
|
||||
.properties([
|
||||
email::Property::Id,
|
||||
email::Property::BlobId,
|
||||
email::Property::ThreadId,
|
||||
email::Property::MailboxIds,
|
||||
email::Property::Keywords,
|
||||
email::Property::ReceivedAt,
|
||||
email::Property::MessageId,
|
||||
email::Property::InReplyTo,
|
||||
email::Property::References,
|
||||
email::Property::Sender,
|
||||
email::Property::From,
|
||||
email::Property::To,
|
||||
email::Property::Cc,
|
||||
email::Property::Bcc,
|
||||
email::Property::ReplyTo,
|
||||
email::Property::Subject,
|
||||
email::Property::SentAt,
|
||||
email::Property::HasAttachment,
|
||||
email::Property::Preview,
|
||||
email::Property::BodyValues,
|
||||
email::Property::TextBody,
|
||||
email::Property::HtmlBody,
|
||||
email::Property::Attachments,
|
||||
email::Property::BodyStructure,
|
||||
])
|
||||
.arguments()
|
||||
.body_properties([
|
||||
email::BodyProperty::PartId,
|
||||
email::BodyProperty::BlobId,
|
||||
email::BodyProperty::Size,
|
||||
email::BodyProperty::Name,
|
||||
email::BodyProperty::Type,
|
||||
email::BodyProperty::Charset,
|
||||
email::BodyProperty::Headers,
|
||||
email::BodyProperty::Disposition,
|
||||
email::BodyProperty::Cid,
|
||||
email::BodyProperty::Language,
|
||||
email::BodyProperty::Location,
|
||||
])
|
||||
.fetch_all_body_values(true)
|
||||
.max_body_value_bytes(100);
|
||||
let email = request
|
||||
.send_get_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap()
|
||||
.into_test();
|
||||
|
||||
// Compare raw message
|
||||
file_name.set_extension("eml");
|
||||
let result = replace_boundaries(String::from_utf8(raw_message).unwrap());
|
||||
|
||||
if fs::read(&file_name).unwrap() != result.as_bytes() {
|
||||
file_name.set_extension("eml_failed");
|
||||
fs::write(&file_name, result.as_bytes()).unwrap();
|
||||
panic!("Test failed, output saved to {}", file_name.display());
|
||||
}
|
||||
|
||||
// Compare response
|
||||
file_name.set_extension("jmap");
|
||||
let result = replace_blob_ids(replace_boundaries(
|
||||
serde_json::to_string_pretty(&email).unwrap(),
|
||||
));
|
||||
if fs::read(&file_name).unwrap() != result.as_bytes() {
|
||||
file_name.set_extension("jmap_failed");
|
||||
fs::write(&file_name, result.as_bytes()).unwrap();
|
||||
panic!("Test failed, output saved to {}", file_name.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(client: &mut Client, root_mailbox_id: &str) {
|
||||
// Obtain all messageIds previously created
|
||||
let mailbox = client
|
||||
.email_query(
|
||||
email::query::Filter::in_mailbox(root_mailbox_id).into(),
|
||||
None::<Vec<_>>,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create two test mailboxes
|
||||
let test_mailbox1_id = client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("Test 1", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
let test_mailbox2_id = client
|
||||
.set_default_account_id(Id::new(1).to_string())
|
||||
.mailbox_create("Test 2", None::<String>, Role::None)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
|
||||
// Set keywords and mailboxes
|
||||
let mut request = client.build();
|
||||
request
|
||||
.set_email()
|
||||
.update(mailbox.id(0))
|
||||
.mailbox_ids([&test_mailbox1_id, &test_mailbox2_id])
|
||||
.keywords(["test1", "test2"]);
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated(mailbox.id(0))
|
||||
.unwrap();
|
||||
assert_email_properties(
|
||||
client,
|
||||
mailbox.id(0),
|
||||
&[&test_mailbox1_id, &test_mailbox2_id],
|
||||
&["test1", "test2"],
|
||||
)
|
||||
.await;
|
||||
|
||||
// Patch keywords and mailboxes
|
||||
let mut request = client.build();
|
||||
request
|
||||
.set_email()
|
||||
.update(mailbox.id(0))
|
||||
.mailbox_id(&test_mailbox1_id, false)
|
||||
.keyword("test1", true)
|
||||
.keyword("test2", false)
|
||||
.keyword("test3", true);
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated(mailbox.id(0))
|
||||
.unwrap();
|
||||
assert_email_properties(
|
||||
client,
|
||||
mailbox.id(0),
|
||||
&[&test_mailbox2_id],
|
||||
&["test1", "test3"],
|
||||
)
|
||||
.await;
|
||||
|
||||
// Orphan messages should not be permitted
|
||||
let mut request = client.build();
|
||||
request
|
||||
.set_email()
|
||||
.update(mailbox.id(0))
|
||||
.mailbox_id(&test_mailbox2_id, false);
|
||||
assert!(matches!(
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated(mailbox.id(0)),
|
||||
Err(Error::Set(SetError {
|
||||
type_: SetErrorType::InvalidProperties,
|
||||
..
|
||||
}))
|
||||
));
|
||||
|
||||
// Updating and destroying the same item should not be allowed
|
||||
let mut request = client.build();
|
||||
let set_email_request = request.set_email();
|
||||
set_email_request
|
||||
.update(mailbox.id(0))
|
||||
.mailbox_id(&test_mailbox2_id, false);
|
||||
set_email_request.destroy([mailbox.id(0)]);
|
||||
assert!(matches!(
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.updated(mailbox.id(0)),
|
||||
Err(Error::Set(SetError {
|
||||
type_: SetErrorType::WillDestroy,
|
||||
..
|
||||
}))
|
||||
));
|
||||
|
||||
// Delete some messages
|
||||
let mut request = client.build();
|
||||
request.set_email().destroy([mailbox.id(1), mailbox.id(2)]);
|
||||
assert_eq!(
|
||||
request
|
||||
.send_set_email()
|
||||
.await
|
||||
.unwrap()
|
||||
.destroyed_ids()
|
||||
.unwrap()
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
let mut request = client.build();
|
||||
request.get_email().ids([mailbox.id(1), mailbox.id(2)]);
|
||||
assert_eq!(request.send_get_email().await.unwrap().not_found().len(), 2);
|
||||
|
||||
// Destroy test mailboxes
|
||||
client
|
||||
.mailbox_destroy(&test_mailbox1_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.mailbox_destroy(&test_mailbox2_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn assert_email_properties(
|
||||
client: &mut Client,
|
||||
message_id: &str,
|
||||
mailbox_ids: &[&str],
|
||||
keywords: &[&str],
|
||||
) {
|
||||
let result = client
|
||||
.email_get(
|
||||
message_id,
|
||||
[email::Property::MailboxIds, email::Property::Keywords].into(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut mailbox_ids_ = result.mailbox_ids().to_vec();
|
||||
let mut keywords_ = result.keywords().to_vec();
|
||||
mailbox_ids_.sort_unstable();
|
||||
keywords_.sort_unstable();
|
||||
|
||||
assert_eq!(mailbox_ids_, mailbox_ids);
|
||||
assert_eq!(keywords_, keywords);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::BTreeSet, sync::Arc};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use jmap::{api::SessionManager, JMAP};
|
||||
use jmap_client::client::{Client, Credentials};
|
||||
|
@ -9,6 +9,7 @@ use crate::{add_test_certs, store::TempDir};
|
|||
|
||||
pub mod email_get;
|
||||
pub mod email_query;
|
||||
pub mod email_set;
|
||||
pub mod thread_get;
|
||||
pub mod thread_merge;
|
||||
|
||||
|
@ -50,9 +51,10 @@ pub async fn jmap_tests() {
|
|||
let delete = true;
|
||||
let mut params = init_jmap_tests(delete).await;
|
||||
//email_get::test(params.server.clone(), &mut params.client).await;
|
||||
email_set::test(params.server.clone(), &mut params.client).await;
|
||||
//email_query::test(params.server.clone(), &mut params.client, delete).await;
|
||||
//thread_get::test(params.server.clone(), &mut params.client).await;
|
||||
thread_merge::test(params.server.clone(), &mut params.client).await;
|
||||
//thread_merge::test(params.server.clone(), &mut params.client).await;
|
||||
if delete {
|
||||
params.temp_dir.delete();
|
||||
}
|
||||
|
@ -84,7 +86,7 @@ async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest {
|
|||
// Create client
|
||||
let mut client = Client::new()
|
||||
.credentials(Credentials::bearer("DO_NOT_ATTEMPT_THIS_AT_HOME"))
|
||||
.timeout(360000)
|
||||
.timeout(Duration::from_secs(60))
|
||||
.accept_invalid_certs(true)
|
||||
.connect("https://127.0.0.1:8899")
|
||||
.await
|
||||
|
@ -143,7 +145,6 @@ pub fn replace_boundaries(string: String) -> String {
|
|||
pub fn replace_blob_ids(string: String) -> String {
|
||||
let values = find_values(&string, "blobId\":");
|
||||
if !values.is_empty() {
|
||||
//let values = BTreeSet::from_iter(values).into_iter().collect::<Vec<_>>();
|
||||
replace_values(
|
||||
string,
|
||||
&values,
|
||||
|
|
|
@ -20,7 +20,7 @@ async fn test_1(db: Arc<Store>) {
|
|||
for id in 0..100 {
|
||||
handles.push({
|
||||
let db = db.clone();
|
||||
tokio::spawn(async move { db.assign_change_id(0, 0).await })
|
||||
tokio::spawn(async move { db.assign_change_id(0).await })
|
||||
});
|
||||
expected_ids.insert(id);
|
||||
}
|
||||
|
|
|
@ -163,7 +163,7 @@ pub async fn test(db: Arc<Store>, do_insert: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
builder.custom(fts_builder).unwrap();
|
||||
builder.custom(fts_builder);
|
||||
documents.lock().unwrap().push(builder.build());
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue