Email/set create tests passing

This commit is contained in:
Mauro D 2023-04-28 08:35:53 +00:00
parent 46b5dc0425
commit c3a2b43ddb
31 changed files with 2257 additions and 310 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/Cargo.lock
.vscode
*.failed
*_failed

View file

@ -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)
}
}
}
}

View file

@ -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!("}}");
}*/
}
}
*/

View file

@ -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()),
);
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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!(),
}
}

View file

@ -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,
}
}
}

View file

@ -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 = []

View file

@ -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 {

View file

@ -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(),
};

View file

@ -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)
}
}

View file

@ -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;
}

View file

@ -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

View file

@ -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 {

View file

@ -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)),

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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(),
})
}
}

View file

@ -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();

View file

@ -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());

View file

@ -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(())
}
}

View 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)?,
})
}
}

View file

@ -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)
}

View file

@ -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(())
}
}

View file

@ -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)]

View file

@ -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
View 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);
}

View file

@ -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,

View file

@ -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);
}

View file

@ -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());
});
}